Chio/Docs

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.

rendering…
Envoy's ext_authz filter calls chio for every request, receives allow/deny plus header mutations, and forwards (or rejects) accordingly.

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:

yaml
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

If Envoy cannot reach chio, the request is denied. That is the safe default for capability enforcement. Change 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:

  1. x-chio-capability-token (preferred, explicit): raw capability token id issued by chio, mapped to AuthMethod::Capability.
  2. 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.
  3. mTLS peer principal: when Envoy reports a downstream peer certificate, its SPIFFE URI or subject DN becomes the caller subject via AuthMethod::Mtls.
  4. 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

text
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

The Istio reference deployment surfaces an 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:

rust
// 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.

rust
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

Envoy ext_authz supports an HTTP-mode transport in addition to gRPC. chio does not expose a native HTTP ext_authz endpoint today. The sidecar ships /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

The reference manifests require Istio 1.22+ (typed-header forwarding stabilised there; 1.20 and 1.21 support gRPC ext_authz but not the header knobs used here) and Kubernetes 1.28+ for the 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:

yaml
# 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: true

Route Traffic to Chio

yaml
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

The adapter is a standard Envoy ext_authz filter, so it applies to any Envoy data plane in principle. The shipped, qualified reference deployment targets Istio only. Consul Connect wiring is a design direction, not a maintained reference today.

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.

FactorSidecarCluster serviceDaemonSet
Latency<1ms1-5ms<1ms
Resource overheadHigh (per pod)Low (shared)Medium (per node)
Policy isolationPer-podCluster-widePer-node
Failure blast radiusSingle podAll podsAll pods on node
Best forHigh-security, multi-tenantDev/staging, low trafficProduction, single-tenant

Latency Budget

ComponentTargetNotes
Envoy filter overhead<0.1msIn-process, negligible
Network to Chio<0.5msLoopback or node-local
Chio evaluation<2msPolicy match plus guard pipeline
Receipt signing<0.5msEd25519, fast
Total ext_authz<3ms p99, <5ms totalOn 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

  1. Deploy chio as a cluster-wide service in the chio-system namespace.
  2. Register chio as an ext_authz provider in the mesh config.
  3. Apply the AuthorizationPolicy CUSTOM rule to a single test workload.
  4. Verify receipts are produced and allow/deny behavior is correct.
  5. Expand via label selectors one workload at a time.

From OPA to Chio

Roadmap

Incremental OPA migration and shadow-mode rollout are design directions, not shipped adapter features. Treat the two patterns below as intended direction, not a maintained path.

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