Skip to content

feat(authz): external authorization via Envoy ext_authz gRPC#36

Merged
polaz merged 4 commits into
mainfrom
feat/#35-ext-authz
Jun 20, 2026
Merged

feat(authz): external authorization via Envoy ext_authz gRPC#36
polaz merged 4 commits into
mainfrom
feat/#35-ext-authz

Conversation

@polaz

@polaz polaz commented Jun 20, 2026

Copy link
Copy Markdown
Member

Summary

Gates proxied API requests through an external authorization server using the Envoy ext_authz gRPC contract (envoy.service.auth.v3.Authorization/Check), so the proxy interoperates out of the box with OPA's Envoy plugin and any ext_authz server.

What's added (when authz.enabled)

  • Before forwarding a transcoded request upstream, the proxy calls Check with a CheckRequest built from the HTTP attributes (method, path, host, scheme, query, headers — including the identity headers the JWT layer injected).
  • Allowed (OK rpc.Status): forward, applying any ok_response headers the authz server returns.
  • Denied: return the denied_response status / headers / body (default 403). A missing status denies (never leaks through).
  • Authz unreachable / timeout: failure_mode_allow chooses fail-open (forward) or fail-closed (503, distinct from a policy 403). Default is fail-closed.
  • Mounted on the proxied API routes only, inside the auth layer, so health / metrics / discovery are never gated and the Check call sees verified identity.

Config reshape

AuthzConfig is reshaped to the ext_authz contract: endpoint (gRPC address), timeout_ms, failure_mode_allow. The prior service / method / *_template fields modeled a different (custom) contract and were unused; the Envoy contract sends the full HTTP context with a fixed service/method.

authz:
  enabled: true
  endpoint: "http://opa:9191"
  timeout_ms: 200
  failure_mode_allow: false

Design

Decision logic (build CheckRequest, interpret CheckResponse, map headers / denied status) is pure and unit-tested; the middleware is thin glue. Uses the envoy-types crate for the generated v3 types.

Testing

7 tests: CheckRequest attribute mapping, scheme default, allow + ok-header collection, missing-status deny, denied-response status mapping, and fail-open / fail-closed against an unreachable authz server. cargo nextest run --features redis: 117 passed. clippy (all-features) + fmt clean.

Out of scope (last roadmap item)

BFF sessions (bff).

Docs

README: External AuthZ under Features; roadmap now tracks only BFF sessions.

Closes #35

Gate proxied API requests through an external authorization server using
the Envoy ext_authz contract (envoy.service.auth.v3.Authorization/Check),
so the proxy interoperates with OPA and any ext_authz server.

- Before forwarding, the proxy calls Check with a CheckRequest built from
  the request's HTTP attributes (method, path, host, scheme, query,
  headers — including the identity headers the JWT layer injected).
- An OK rpc status allows the request and applies any ok_response
  headers; a denied response returns its status / headers / body
  (default 403); a missing status denies.
- When the authz call fails (unreachable / timeout), failure_mode_allow
  chooses fail-open (forward) or fail-closed (503); default is closed.
- Mounted on the proxied API routes only, inside the auth layer, so
  health / metrics / discovery are never gated.

AuthzConfig is reshaped to the ext_authz contract (endpoint, timeout_ms,
failure_mode_allow); the prior service/method/template fields modeled a
different contract and were unused.

Closes #35
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 90953b48-0023-4f19-93ce-c5699302d73d

📥 Commits

Reviewing files that changed from the base of the PR and between 954246e and 57b0385.

📒 Files selected for processing (2)
  • src/auth/authz.rs
  • src/config.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added external authorization support via Envoy ext_authz gRPC, including configurable timeout and failure mode (fail-open vs fail-closed).
    • External authorization is now applied to proxied transcoding routes, with upstream requests forwarded on allow and clear HTTP responses on deny/unavailable.
  • Bug Fixes

    • Improved behavior when the authz service is unreachable by returning a consistent fail-open or fail-closed response.
  • Documentation

    • Updated the README roadmap to split “External AuthZ,” “Zero code changes between services,” and “BFF sessions” with clearer wording.
  • Chores

    • Extended dependencies to support the new ext_authz contract.

Walkthrough

Adds Envoy ext_authz gRPC external authorization middleware to the proxy. AuthzConfig is reshaped to endpoint, timeout_ms, and failure_mode_allow fields. A new Authz client and middleware function are implemented in src/auth/authz.rs, wired conditionally onto transcoding routes only. The envoy-types 0.7 dependency is added, and the README roadmap is updated.

Changes

Envoy ext_authz gRPC authorization

Layer / File(s) Summary
AuthzConfig contract reshape
src/config.rs, Cargo.toml
AuthzConfig fields replaced: service/method/*_template removed; endpoint, timeout_ms (default 200), and failure_mode_allow added. envoy-types = "0.7" added as a dependency. Test YAML fixture and new optional-endpoint test added to validate the configuration shape.
Authz client type and build
src/auth/authz.rs, src/auth/mod.rs
Authz struct with build associated function: validates the gRPC endpoint, constructs a lazy Tonic channel, stores timeout and failure mode, returns None when disabled. Module publicly re-exported via pub mod authz;.
Middleware, helpers, and response rendering
src/auth/authz.rs
middleware builds a CheckRequest from Axum request headers/method/URI, calls AuthorizationClient::check, and branches on allow (header injection + forward), deny (DeniedHttpResponse rendered), or gRPC failure (fail-open pass-through or fail-closed 503). Supporting helpers: build_check_request, evaluate, header_kv, denied_to_response, service_unavailable. Comprehensive unit tests cover attribute mapping, scheme defaulting, allow/deny/fail-open/fail-closed paths, and header injection.
Router wiring
src/lib.rs
transcode_routes changed to mutable; conditionally wrapped with axum::middleware::from_fn_with_state(authz, auth::authz::middleware) when authz is configured. Health, metrics, OIDC, and OpenAPI routes are not wrapped.
README roadmap update
README.md
Combined roadmap bullet split into three separate items: External AuthZ (Envoy ext_authz gRPC with fail-open/closed control), Zero code changes (same binary/different config), and BFF sessions.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ProxyRouter
  participant authz_middleware
  participant AuthorizationClient as Envoy ext_authz server
  participant TranscodeUpstream

  Client->>ProxyRouter: HTTP request (transcoding route)
  ProxyRouter->>authz_middleware: dispatch via middleware layer
  authz_middleware->>authz_middleware: build_check_request(headers, method, URI, scheme)
  authz_middleware->>AuthorizationClient: Check(CheckRequest) with timeout
  alt Allow (RPC status OK)
    AuthorizationClient-->>authz_middleware: CheckResponse { ok_response headers }
    authz_middleware->>TranscodeUpstream: forward + inject ok_response headers
    TranscodeUpstream-->>Client: upstream response
  else Deny (non-zero RPC status or denied_response)
    AuthorizationClient-->>authz_middleware: CheckResponse { denied_response }
    authz_middleware-->>Client: 403 (or authz-specified status/headers/body)
  else gRPC failure (timeout / unreachable)
    AuthorizationClient-->>authz_middleware: error
    alt failure_mode_allow=true
      authz_middleware->>TranscodeUpstream: forward (fail-open)
      TranscodeUpstream-->>Client: upstream response
    else failure_mode_allow=false
      authz_middleware-->>Client: 503 {"error":"UNAVAILABLE"}
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • structured-world/structured-proxy#34: Both PRs conditionally wire auth-related middleware/routes in ProxyServer::router in src/lib.rs (this PR adds ext_authz gRPC on transcoding routes; retrieved PR adds forward-auth verify endpoint), so they are directly related at the routing/auth wiring level.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(authz): external authorization via Envoy ext_authz gRPC' clearly and concisely summarizes the primary change: implementing Envoy ext_authz gRPC-based external authorization.
Description check ✅ Passed The description comprehensively explains the feature implementation, configuration changes, design decisions, and testing coverage, all directly related to the changeset.
Linked Issues check ✅ Passed The PR implements all objectives from issue #35: Envoy ext_authz gRPC integration with CheckRequest construction from HTTP attributes, allow/deny/fail-open/fail-closed handling, and README documentation.
Out of Scope Changes check ✅ Passed All changes are directly related to external authorization via Envoy ext_authz gRPC; no unrelated modifications to other features or systems are present.
Docstring Coverage ✅ Passed Docstring coverage is 94.74% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#35-ext-authz

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/auth/authz.rs`:
- Around line 80-84: The error from the failed authz gRPC call is being
discarded without logging. In the two error handling branches (the `Err(_)`
pattern matches in the failure_mode_allow guard and the subsequent Err branch),
capture the actual error by changing the pattern from `Err(_)` to `Err(e)` to
bind the error value. Then add appropriate logging before returning: for the
fail-open case where `authz.failure_mode_allow` is true, log at warn level with
the error details to help operators diagnose connectivity issues; for the
fail-closed case, log at error level since this results in a 503 service
unavailable response. This will improve observability without changing the
current error handling behavior.

In `@src/config.rs`:
- Around line 579-583: The test in src/config.rs only verifies that config.auth
is Some but does not assert that the authz configuration fields were actually
parsed correctly. After the existing assertion for config.auth.is_some(), add
additional assertions to verify the authz configuration by unwrapping the auth
object, accessing the authz field, and then asserting that enabled is true,
endpoint equals the expected URL string "http://opa:9191", timeout_ms equals
200, and failure_mode_allow is false. This ensures all authz YAML fields were
parsed and deserialized into the correct values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2da4dd2b-ecbe-4202-b43a-0911f5905fac

📥 Commits

Reviewing files that changed from the base of the PR and between 9e7d2b7 and 954246e.

📒 Files selected for processing (6)
  • Cargo.toml
  • README.md
  • src/auth/authz.rs
  • src/auth/mod.rs
  • src/config.rs
  • src/lib.rs

Comment thread src/auth/authz.rs
Comment thread src/config.rs
@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements the Envoy ext_authz gRPC integration that was the last major unshipped roadmap feature, wiring envoy.service.auth.v3.Authorization/Check as a thin axum middleware in front of the transcoded-proxy routes only. The design is sound: JWT runs first (outer layer on the full router) so the authz Check sees verified identity headers; health/metrics/discovery remain ungated; fail-closed/open behaviour and the allowed/denied response mapping all follow the Envoy contract.

  • Core logic (src/auth/authz.rs): build_check_request, evaluate, and denied_to_response are pure functions that are unit-tested well; the middleware itself is minimal glue. The previously noted HeaderMap::insertappend fix and the missing-status-denies safety property are both in place and tested.
  • Config (src/config.rs): AuthzConfig cleanly replaces the old custom-contract fields with endpoint, timeout_ms, and failure_mode_allow; #[serde(default)] on endpoint lets a disabled block omit the field, addressed from the earlier review.
  • One open gap (header_kv): HeaderValueOption.header_action (and the deprecated append field) is ignored — the code always appends authz-supplied headers rather than conditionally replacing, which diverges from the full Envoy contract when an authz server signals overwrite semantics.

Confidence Score: 5/5

Safe to merge; the authz gating is correct and fail-closed by default, all previous review issues are addressed, and tests cover the critical allow/deny/fail-open/fail-closed paths.

The middleware wiring, allow/deny logic, and config reshaping are all correct. The single gap — HeaderValueOption.header_action not being honoured — only matters when an authz server explicitly signals header-replace semantics, which is uncommon in OPA-style deployments. It does not affect security or correctness for the standard inject-new-headers use case.

src/auth/authz.rs — specifically the header_kv function and apply_headers call sites if you later need full Envoy contract fidelity for authz servers that use overwrite semantics.

Important Files Changed

Filename Overview
src/auth/authz.rs New module implementing the Envoy ext_authz gRPC middleware; logic is clean and well-tested, with one contract deviation: HeaderValueOption.append semantics are not honored (always appends regardless of authz server intent)
src/lib.rs Authz middleware correctly wired onto transcode routes only, before JWT auth is applied to the full router, so Check sees injected identity headers
src/config.rs AuthzConfig reshaped to the ext_authz contract; all fields have correct defaults; disabled block can omit endpoint; round-trip tests pass
Cargo.toml Adds envoy-types 0.7 dependency for generated v3 proto types
src/auth/mod.rs Exposes the new authz submodule as pub; no other changes
README.md Moves ext_authz from roadmap to the features list; promotes BFF sessions as the only remaining roadmap item

Reviews (2): Last reviewed commit: "fix(authz): default authz endpoint and p..." | Re-trigger Greptile

Comment thread src/config.rs Outdated
Comment thread src/auth/authz.rs Outdated
polaz added 3 commits June 20, 2026 08:28
- Log a warning (with the gRPC error) when the ext_authz Check call
  fails, noting whether the request is being failed open or closed, so
  operators can diagnose authz connectivity.
- Assert the parsed authz fields (enabled, endpoint, timeout_ms,
  failure_mode_allow) in the full-config deserialization test.

Part of #35
A disabled `authz` block (`enabled: false`) without an `endpoint` fails
deserialization with a missing-field error, forcing a dummy URL to stub
authz out. Fails on current code; fix follows.

Part of #35
- Give authz.endpoint a serde default so a disabled block
  (`authz: { enabled: false }`) parses without supplying a dummy URL;
  build() still short-circuits on !enabled before parsing the channel.
- Apply authz-supplied response headers with append, not insert, so
  multiple values for the same name (e.g. Set-Cookie) are all preserved
  instead of collapsing to the last. Applies to both the allowed-request
  header injection and the denied response.

Part of #35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(authz): external authorization via Envoy ext_authz gRPC

1 participant