BuildGateways
Envoy ext_authz
Configure Envoy ext_authz to obtain Chio allow or deny decisions for services behind Envoy or Istio.
Why Use ext_authz
Every other Chio integration targets a single runtime, framework, or language. ext_authz operates at the proxy boundary. One adapter can evaluate requests for services in a mesh that runs Envoy as the data plane, regardless of the service's language, deployment model, or framework. Istio uses ext_authz natively via AuthorizationPolicy CUSTOM actions; Consul Connect via envoy_extensions.ext_authz; AWS App Mesh, Gloo, standalone Envoy, and Cilium's L7 policy layer all support the same filter.
The protocol fit is almost exact. Envoy expects the external service to accept a request, return allow or deny, and optionally inject headers. Chio's evaluation engine already produces exactly this shape, so teams can configure an existing Envoy deployment to call the adapter. The proxy remains the only sidecar needed by the workload.
The ext_authz Protocol
Envoy's external authorization filter intercepts every request (or a configured subset) and sends a check request to an external service before forwarding upstream. That service returns allow or deny, with optional header mutations applied to the request going upstream or to the response going back to the client.
Two Transport Modes
ext_authz can talk to the external service over gRPC or HTTP. Today chio-envoy-ext-authz ships the gRPC path (HTTP/2, binary protobuf) via the generated envoy.service.auth.v3.Authorization service. A native HTTP-mode adapter is not yet shipped; see HTTP Mode Adapter below for detail.
Envoy Filter Configuration
A minimum gRPC-mode filter chain wiring Chio into Envoy:
http_filters:
- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
grpc_service:
envoy_grpc:
cluster_name: chio_ext_authz
timeout: 0.25s
with_request_body:
max_request_bytes: 8192
allow_partial_message: true
pack_as_bytes: true
allowed_headers:
patterns:
- exact: authorization
- prefix: x-chio-
# Fail-closed: deny if chio is unreachable
failure_mode_allow: false
include_peer_certificate: true
clusters:
- name: chio_ext_authz
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: chio_ext_authz
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: 127.0.0.1, port_value: 9091 }Fail-closed is the default
failure_mode_allow to true only for non-security-critical workloads where availability outweighs enforcement.Mapping ext_authz to Chio
The ext_authz CheckRequest carries the downstream request's attributes. The adapter projects these onto Chio's protocol-agnostic request model: method and path map directly; Authorization and x-chio-capability-token headers produce caller identity and capability id; the request body is hashed to SHA-256 and never stored in full; the source principal (mTLS peer certificate or SPIFFE ID) becomes the caller subject; Envoy's request id flows through as request_id for correlation.
Capability Token Transport
The adapter derives caller identity from four sources, checked in order. The first one present wins:
- x-chio-capability-token (preferred, explicit): raw capability token id issued by chio, mapped to
AuthMethod::Capability. - Authorization: Bearer <token>: standard bearer flow, mapped to
AuthMethod::Bearer. The token is reduced to a SHA-256 digest immediately; the raw value never leaves the translation layer. - mTLS peer principal: when Envoy reports a downstream peer certificate, its SPIFFE URI or subject DN becomes the caller subject via
AuthMethod::Mtls. - If none is present, the request is evaluated under
AuthMethod::Anonymous. Your policy decides whether anonymous callers are allowed.
The adapter never forwards raw secrets upstream. It strips authorization and x-chio-capability-token from the forwarded header map, and reduces bearer tokens and request bodies to SHA-256 hex digests before they cross the translation boundary, consistent with Chio's never-store-secrets policy.
Verdict to CheckResponse
chio Verdict::Allow
-> CheckResponse { status: OK (0) }
+ OkHttpResponse { headers: [] } # no header mutations
+ dynamic_metadata { "chio.verdict": "allow" }
chio Verdict::Deny { reason, guard, http_status }
-> CheckResponse { status: PermissionDenied (7) }
+ DeniedHttpResponse {
status: StatusCode(<http_status>), # nearest Envoy code, default 403
headers: [
{ "x-chio-denial-reason": "<reason>" },
{ "x-chio-denial-guard": "<guard>" },
],
body: "{\"verdict\":\"deny\",\"reason\":...,\"guard\":...}"
}
+ dynamic_metadata {
"chio.verdict": "deny",
"chio.denial_reason": "<reason>",
"chio.denial_guard": "<guard>",
"chio.http_status": <admitted-code>
}
# Translation or kernel error -> fail closed
-> CheckResponse { status: Internal (13) }
+ DeniedHttpResponse { status: 500, body: "<generic deny>" }
+ dynamic_metadata { "chio.verdict": "deny", "chio.fail_closed": true }The Verdict type carries no receipt id, so the adapter attaches none on allow: an allow is an empty OkHttpResponse with no header mutations. Correlation data instead lands in Envoy dynamic metadata under the chio.* namespace (chio.verdict, chio.denial_reason, chio.denial_guard, chio.http_status, chio.fail_closed), which Envoy surfaces to access logs and downstream filters. A deny sets x-chio-denial-reason and x-chio-denial-guard on the response, with control characters sanitised to spaces.
Receipt-id headers come from the deployment, not this crate
x-chio-receipt-id (and x-chio-policy-hash) header on allowed responses. That behavior comes from the kernel and mesh wrapper around the adapter, not from chio-envoy-ext-authz itself: the crate's translate/response layer emits denial headers and dynamic metadata only. Wire receipt-id propagation in your EnvoyKernel implementation if you need it on the allow path.gRPC Adapter
The adapter implements envoy.service.auth.v3.Authorization/Check as a thin shim over a pluggable kernel trait. The production crate is chio-envoy-ext-authz. It exposes one service type and one trait:
// From crates/protocol/chio-envoy-ext-authz/src/service.rs
/// Kernel abstraction. Implementations delegate to chio-kernel, to
/// HttpAuthority in chio-http-core, or to a test stub.
#[async_trait]
pub trait EnvoyKernel: Send + Sync + 'static {
async fn evaluate(
&self,
request: ToolCallRequest,
) -> Result<Verdict, KernelError>;
}
/// Generic over the kernel implementation. Each CheckRequest is
/// translated to a ToolCallRequest, handed to K::evaluate, and the
/// returned Verdict is mapped back onto a compliant CheckResponse.
pub struct ChioExtAuthzService<K: EnvoyKernel> {
kernel: K,
}
impl<K: EnvoyKernel> ChioExtAuthzService<K> {
pub fn new(kernel: K) -> Self { Self { kernel } }
}
#[async_trait]
impl<K: EnvoyKernel> Authorization for ChioExtAuthzService<K> {
async fn check(
&self,
request: Request<CheckRequest>,
) -> Result<Response<CheckResponse>, Status> { /* ... */ }
}A minimal binding. Any type that implements EnvoyKernel plugs in; for production, write a small adapter that delegates to chio-kernel or to HttpAuthority in chio-http-core.
use async_trait::async_trait;
use chio_envoy_ext_authz::{
proto::envoy::service::auth::v3::authorization_server::AuthorizationServer,
translate::{ToolCallRequest, Verdict},
ChioExtAuthzService, EnvoyKernel, KernelError,
};
struct MyKernel;
#[async_trait]
impl EnvoyKernel for MyKernel {
async fn evaluate(
&self,
_request: ToolCallRequest,
) -> Result<Verdict, KernelError> {
// Delegate to chio-kernel / HttpAuthority / custom policy here.
Ok(Verdict::Allow)
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let svc = ChioExtAuthzService::new(MyKernel);
tonic::transport::Server::builder()
.add_service(AuthorizationServer::new(svc))
.serve("0.0.0.0:9091".parse()?)
.await?;
Ok(())
}HTTP Mode Adapter
HTTP mode is not yet shipped
/chio/evaluate, /chio/verify, /chio/health, approval admin routes, and the capability mint/release surface. There is no /ext_authz endpoint, and the evaluate endpoint does not translate Envoy's HTTP-mode request conventions. Until an HTTP-mode adapter ships, use the gRPC adapter above; an Envoy config pointed at a non-existent HTTP endpoint will fail closed on every request.Istio Integration
Istio is the most common Envoy-based mesh. chio plugs into Istio via AuthorizationPolicy with the CUSTOM action, which delegates the decision to an ext_authz provider.
Chio layers on top of Istio's RBAC. Istio answers "can service A talk to service B." Chio answers "does this agent have a valid capability token for this specific tool invocation, within budget, and passing all guards." Istio RBAC covers service identity (via mTLS and SPIFFE IDs) and coarse allow/deny at the path level. Chio covers per-tool capability, signed receipts, the guard pipeline (WASM, Rego, built-in), and per-capability budget limits, none of which Istio native RBAC models.
Version prerequisites
security.istio.io/v1 GA API the policies use. Deploy a dedicated ext_authz adapter image (the reference uses ghcr.io/backbay-labs/chio-ext-authz as a placeholder). Do not point the provider at ghcr.io/backbay-labs/chio-sidecar: that is the HTTP sidecar image and does not expose Envoy's gRPC Authorization/Check service.Register Chio as an ext_authz Provider
chio-ext-authz here is the Istio provider name, referenced later by provider.name. The backing Kubernetes Service is the gRPC adapter Deployment, exposed on port 9091:
# istio-configmap or IstioOperator overlay
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
meshConfig:
extensionProviders:
- name: chio-ext-authz
envoyExtAuthzGrpc:
service: chio-sidecar.chio-system.svc.cluster.local
port: 9091
timeout: 0.25s
includeRequestHeadersInCheck:
- authorization
- x-chio-capability-token
- x-chio-session-id
- x-request-id
includePeerCertificate: trueRoute Traffic to Chio
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: chio-tool-authorization
namespace: agent-tools
spec:
# Apply only to opted-in tool server workloads
selector:
matchLabels:
chio.world/secured: "true"
action: CUSTOM
provider:
name: chio-ext-authz
rules:
- to:
- operation:
# Guard pipeline handles fine-grained decisions
paths: ["/*"]Not every service needs chio. The chio.world/secured: "true" label gates which workloads go through chio evaluation. Services without the label fall back to standard Istio RBAC only.
SPIFFE / SPIRE Workload Identity
In a mesh that issues SPIFFE IDs via SPIRE, the source principal in the ext_authz CheckRequest carries the SPIFFE ID. The Chio adapter lifts it into the normalized workloadIdentity record so policy and receipts bind to the workload, not the transport credential. For the full shape, the three credential kinds, and how this interacts with runtime-assurance tiers, see the Workload Identity concept. The operational recipe for gating tools on a SPIFFE ID lives in the Bind Workload Identity guide.
Consul Connect
Roadmap — Istio is the shipped reference
Consul Connect uses Envoy as its data plane, so the same filter applies. The configuration surface differs (HCL instead of YAML, service-defaults instead of Istio CRDs), but the semantics are the same. Consul Intentions would handle service-to-service authorization (L4 identity) while Chio handles capability-level authorization (L7 tool policy): the two layer cleanly, with Consul deciding whether the agent-orchestrator can talk to the code-execution tool and Chio deciding whether the specific capability token it presents is valid for the invocation it is about to make.
Deployment Topologies
Three topologies are possible; the right pick depends on resource budget, tenant isolation needs, and how much latency you can absorb. The shipped reference deployment runs the cluster-service model — a chio-sidecar Deployment and Service in the chio-system namespace. The latency figures below are design targets, not measured production numbers.
Sidecar (per pod)
Chio runs in the same pod as Envoy. ext_authz calls traverse loopback. Lowest latency, strongest isolation, highest resource footprint.
Cluster service
Chio runs as a centralized Deployment behind a service. Every Envoy proxy calls it over the cluster network. Simplest operationally, slightly higher latency (1-5ms in-cluster).
DaemonSet (per node)
Chio runs one instance per node. Envoy sidecars call the node-local instance. Good middle ground between the two extremes.
| Factor | Sidecar | Cluster service | DaemonSet |
|---|---|---|---|
| Latency | <1ms | 1-5ms | <1ms |
| Resource overhead | High (per pod) | Low (shared) | Medium (per node) |
| Policy isolation | Per-pod | Cluster-wide | Per-node |
| Failure blast radius | Single pod | All pods | All pods on node |
| Best for | High-security, multi-tenant | Dev/staging, low traffic | Production, single-tenant |
Latency Budget
| Component | Target | Notes |
|---|---|---|
| Envoy filter overhead | <0.1ms | In-process, negligible |
| Network to Chio | <0.5ms | Loopback or node-local |
| Chio evaluation | <2ms | Policy match plus guard pipeline |
| Receipt signing | <0.5ms | Ed25519, fast |
| Total ext_authz | <3ms p99, <5ms total | On localhost |
Optimization levers: Envoy maintains persistent gRPC connections to Chio so no per-request connection setup is paid; Chio caches compiled policy in memory and hot-swaps on reload; guard results can be cached for idempotent guards keyed on capability token plus route; receipt signing is synchronous but receipt persistence is async, so disk I/O does not block the response.
Migration Paths
From No Auth to Chio
- Deploy chio as a cluster-wide service in the
chio-systemnamespace. - Register chio as an ext_authz provider in the mesh config.
- Apply the AuthorizationPolicy CUSTOM rule to a single test workload.
- Verify receipts are produced and allow/deny behavior is correct.
- Expand via label selectors one workload at a time.
From OPA to Chio
Roadmap
The intended path for organizations already running Open Policy Agent with ext_authz is an incremental one: register Chio alongside OPA as a second provider, move policies onto Chio guards, validate parity, then retire the OPA provider. Because Istio scopes each AuthorizationPolicy to a provider by name, the two can run side by side on disjoint label selectors during the cutover.
Shadow Mode
For risk-averse rollouts the goal is to evaluate and sign a receipt for every request while still returning allow, so the shadow receipts can be analysed before enforcement is switched on. The enforcing/observe split is a deployment concern of the EnvoyKernel implementation, not a toggle the adapter itself exposes.
Next Steps
- Receipt Dashboard · visualize the receipts produced by the ext_authz adapter
- AWS Lambda · the serverless counterpart to the ext_authz sidecar model
- Budgets · per-capability spending envelopes, enforced on every request