Chio/Docs

BuildTopologiesnew

Sidecar HTTP Service

Run Chio as a separate process and image. The host calls the kernel over local HTTP, while the signing key stays in the sidecar.

When to read this page

This is the operational deep dive. For the trust-boundary discussion and the high-level choice between sidecar and in-process, see Deployment Topologies.

Why Sidecar

  • Independent trust boundary. The Ed25519 signing key is mounted into the sidecar container and is unreachable from the host container. A compromised host cannot forge receipts.
  • Independent scaling. Roll the kernel forward without redeploying the host. Useful when kernel and host are owned by different teams.
  • Language portability. The host can be any language that speaks HTTP. The same sidecar image serves Rust, Python, Go, Node, .NET, and Java hosts.
  • Platform support. Cloud Run, ECS Fargate, and Azure Container Apps all support multi-container deployments with localhost networking and startup ordering.

The cost is a localhost-HTTP hop on every kernel call (about 100 microseconds per call). Negligible for tool-call workloads, noticeable on hot inner loops.

rendering…
The sidecar runs in its own container alongside the host. The host reaches the kernel through localhost:9090. The signing key is mounted into the sidecar only.

The Sidecar Image

Two reference Dockerfiles ship in the Chio repository:

PathBaseUse
deploy/docker/Dockerfile.sidecarAlpine 3.22 + tiniDefault sidecar image. Small, musl-linked, runs as uid/gid 10001.
deploy/sidecar/Dockerfiledistroless cc-debian12 nonroot (uid 65532)Distroless variant with a baked-in curl for HEALTHCHECK and a multi-arch shared-library layout.

Both compile the same chio binary from chio-cli. The Alpine build runs it as /usr/local/bin/chio under tini; the distroless build installs it as /usr/local/bin/chio-sidecar and enters it directly, with no shell and no tini. The difference is the runtime layer. For a deeper Dockerfile-by-Dockerfile breakdown, see Container Images.


Build

From the Chio repository root:

bash
docker build -f deploy/docker/Dockerfile.sidecar -t chio-sidecar:local .

The builder stage installs protoc because chio-envoy-ext-authz (reachable transitively through the workspace) invokes tonic-build at compile time. CI installs protobuf-compiler for the same reason. Keeping the Docker build consistent with CI prevents a future dependency change from silently breaking the image.

The builder also installs openssl-dev and openssl-libs-static and builds with OPENSSL_STATIC=1. The chio-custody-hw crate, reachable through chio-kernel -> chio-custody-hw, pulls in webauthn-rs-core, which links against openssl-sys. Static linking keeps the runtime layer to CA roots plus tini, with no libssl.so runtime dependency.

The build copies the full workspace so path dependencies resolve. Specifically:

  • wit/ is consumed by chio-wasm-guards via wasmtime::component::bindgen! (reached through the chio-cli -> chio-wasm-guards path dependency).
  • contracts/ is embedded by chio-web3-bindings through Alloy's sol! macro.
  • bench/, examples/, formal/, tests/, integrations/ (the aws-bedrock control plane and mcp-adapter members), and xtask/ are workspace members; their absence breaks Cargo.lock resolution.
  • sdks/ is copied for forward-compatibility with non-workspace SDKs that may join the root workspace.

For the distroless variant:

bash
docker build -f deploy/sidecar/Dockerfile -t ghcr.io/backbay-labs/chio-sidecar:latest .

Use that command to build the sidecar deploy manifests. deploy/sidecar/Dockerfile adds a stage that bakes curl plus its shared libraries into the runtime image so the distroless layer can run a HEALTHCHECK probe without a shell.


Run

The image's zero-argument default is --help. Every chio subcommand needs operator input (policy path, wrapped server command, etc.), so a bare docker run prints usage and exits successfully after printing usage. Override CMD at deploy time.

bash
docker run --rm -p 9090:9090 chio-sidecar:local <subcommand> [args...]

The deployment uses these two subcommands: chio api protect (reverse proxy in front of an OpenAPI host) and chio mcp serve-http (HTTP-fronted MCP tool server). Both are configured entirely by CLI flags; there is no config file to mount or environment-variable configuration. For example: api protect invocation:

bash
chio \
  --authority-seed-file /etc/chio/seed/authority.seed \
  api protect \
    --upstream http://127.0.0.1:8080 \
    --spec /etc/chio/spec/openapi.yaml \
    --listen 0.0.0.0:9090 \
    --receipt-store /var/lib/chio/receipts.db

--upstream is the only required flag. --spec is auto-discovered when omitted; the kernel derives its route and scope table from that OpenAPI document. --listen defaults to 127.0.0.1:9090 (the manifests bind 0.0.0.0:9090). --receipt-store points at a SQLite audit log; pass --allow-ephemeral-receipts to run in-memory instead. The signing seed is the global --authority-seed-file flag, a file path the kernel auto-generates and persists 0600 if it is missing, never an env var carrying key material.

The mcp serve-http mode takes the wrapped MCP server command as a required trailing argument after --:

bash
chio mcp serve-http \
  --policy /etc/chio/policy.yaml \
  --server-id my-tool \
  -- npx @modelcontextprotocol/server-filesystem /data

For a zero-config on-ramp there is also chio start, a convenience alias for api protect aimed at SDK-quickstart and chio-hermes users. It runs the same router with no upstream proxy, durable receipts by default (pass --allow-ephemeral-receipts for the in-memory quickstart), a default --listen 127.0.0.1:9090 matching ChioClient.DEFAULT_BASE_URL in the Python SDK, and a --print-config flag that prints a chio-hermes config snippet on startup.

Zero-argument behavior

chio api protect requires --upstream, and chio mcp serve-http requires --policy, --server-id, plus the trailing wrapped-server command. The image falls through to --help on a bare invocation and exits successfully. It does not open the health endpoint.

Listen Address

The bind address is the --listen flag, default 127.0.0.1:9090. Every reference manifest overrides it to 0.0.0.0:9090 so the co-located application container can reach the kernel over the shared network namespace:

bash
chio api protect --upstream http://127.0.0.1:8080 --listen 0.0.0.0:9090

The health routes are fixed at /chio/health and /chio/live; they are not configurable. The application container reaches the kernel through http://localhost:9090 because pod / task / revision containers share the network namespace.


Health Endpoints

The sidecar exposes two distinct routes with different jobs:

  • GET /chio/live — liveness. Process-only. Returns 200 while the process runs. A dependency blip deliberately does not trip it, so an orchestrator never recycles a container that is still serving correctly.
  • GET /chio/health — readiness. Dependency-aware. Returns 503 when the receipt store can no longer persist, which pulls the instance from rotation on a post-startup outage instead of routing traffic to a sidecar that can only deny. Startup and readiness probes gate on this route.
  • Fail closed: if the kernel cannot load its mounted OpenAPI spec or open its durable receipt store at startup, the process exits non-zero. The orchestrator marks the container unhealthy (ECS, Azure) or fails the revision (Cloud Run).

Both routes return a HealthResponse body. The status field serializes to one of healthy / degraded / unhealthy; readiness also reports the embedded kernel's receipt_backend and revocation_backend (durable, ephemeral, or, for revocation, remote).

json
{"status":"healthy","version":"0.1.0","receipt_backend":"durable","revocation_backend":"durable"}

The distroless sidecar bakes in a HEALTHCHECK that curls the process-only liveness route, so a transient receipt-store issue does not recycle a serving container:

dockerfile
HEALTHCHECK --interval=10s --timeout=5s --start-period=15s --retries=3 \
  CMD ["/usr/bin/curl", "-fsS", "http://localhost:9090/chio/live"]

Environment Variables

The sidecar accepts operational configuration through CLI flags. It has no environment variables for the listen address, health path, kernel configuration, policy source, or receipt sink, and it never reads the signing seed from the environment. Its small fixed set of environment variables is auxiliary to those flags:

VariableDefaultPurpose
RUST_LOGinfoTracing log level read by tracing_subscriber. The image sets it; the manifests also carry a plain CHIO_LOG_LEVEL value by convention.
CHIO_TRUSTED_ISSUER_KEY(none)Single trusted issuer public key (hex-encoded Ed25519). Read by chio api protect.
CHIO_TRUSTED_ISSUER_KEYS(none)Multiple trusted issuer keys, comma-separated. Read alongside CHIO_TRUSTED_ISSUER_KEY; the union is used.
CHIO_SIDECAR_CONTROL_TOKEN(none)Bearer token gating the sidecar admin and /metrics endpoints. Loopback callers need no token; remote callers must present it. Falls back to CHIO_API_PROTECT_CONTROL_TOKEN.

Source: crates/products/chio-cli/src/cli/runtime.rs

CHIO_TRUSTED_ISSUER_KEY, CHIO_TRUSTED_ISSUER_KEYS, and CHIO_SIDECAR_CONTROL_TOKEN are read directly by chio api protect at startup. CHIO_API_PROTECT_CONTROL_TOKEN is the legacy alias. The bind address, OpenAPI spec, receipt store, and signing seed are the --listen, --spec, --receipt-store, and --authority-seed-file flags, not env vars.

Wire Protocol

The sidecar exposes the kernel API over HTTP. Hosts call endpoints for capability validation, tool dispatch, and receipt retrieval; the wire format is documented in Wire Protocol. Every request is serialized as JSON; binary payloads are base64-encoded.

For the chio api protect mode the sidecar is itself the front door: it terminates the inbound request, validates capabilities, runs guards, forwards to the upstream host, and signs a receipt on the way out. The host never sees the original capability token.


Graceful Shutdown

On SIGTERM the sidecar runs a drain sequence:

  1. Stop accepting new connections on the listen address.
  2. Let in-flight requests finish. A request that has already passed the guard pipeline is allowed to complete and emit a receipt.
  3. Flush the receipt log to the receipt store (the SQLite database at --receipt-store, or the in-memory log under --allow-ephemeral-receipts).
  4. Drain the signing task and emit a final checkpoint if a checkpoint boundary is crossed.
  5. Exit 0.

In the Alpine image tini is PID 1, so signals reach the chio binary correctly. The distroless image has no tini: it enters the chio-sidecar binary directly as PID 1, and the binary installs its own SIGTERM handler. If the drain takes longer than the orchestrator's grace period, the platform escalates to SIGKILL; in-flight receipts buffered in memory at that point may be lost.

Set a grace period that fits the receipt store

The drain time is dominated by the final receipt-store flush and checkpoint. Set the orchestrator's termination grace period to at least 30 seconds so an in-flight append is not cut off by SIGKILL.

Process Supervision

systemd

For bare-metal or VM hosts, run the sidecar under systemd with a unit that pins the binary, mounts the secret directory, and restarts on failure.

/etc/systemd/system/chio-sidecar.service
[Unit]
Description=Chio sidecar HTTP service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=chio
Group=chio
ExecStart=/usr/local/bin/chio \
    --authority-seed-file /etc/chio/seed/authority.seed \
    api protect \
    --upstream http://127.0.0.1:8080 \
    --spec /etc/chio/spec/openapi.yaml \
    --listen 0.0.0.0:9090 \
    --receipt-store /var/lib/chio/receipts.db
Environment=RUST_LOG=info
EnvironmentFile=/run/secrets/chio.env
Restart=on-failure
RestartSec=2s
TimeoutStopSec=30s
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/chio

[Install]
WantedBy=multi-user.target

Container orchestrators

For multi-container platforms, the sidecar ships next to the host and the platform handles startup ordering and health gating. The orchestrator-specific manifests live under deploy/:

  • Cloud Run uses run.googleapis.com/container-dependencies plus an httpGet startup probe.
  • ECS Fargate uses dependsOn with condition: HEALTHY and a curl-based healthCheck.
  • Azure Container Apps uses Bicep multi-container probe sequencing.

Worked Example

Run the sidecar locally, send a request, observe the receipt.

bash
# 1. Build the image from the chio repo root.
$ docker build -f deploy/docker/Dockerfile.sidecar -t chio-sidecar:local .

# 2. Run the sidecar in front of a local upstream. The authority seed is
#    auto-generated and persisted under the writable CHIO_HOME; receipts
#    stay in memory for this local run.
$ docker run --rm \
    -p 9090:9090 \
    -v "$(pwd)/openapi.yaml:/etc/chio/spec/openapi.yaml:ro" \
    chio-sidecar:local \
    --authority-seed-file /var/lib/chio/authority.seed \
    api protect \
      --upstream http://host.docker.internal:8080 \
      --spec /etc/chio/spec/openapi.yaml \
      --listen 0.0.0.0:9090 \
      --allow-ephemeral-receipts

# 3. From another shell: readiness (dependency-aware) and liveness.
$ curl -fsS http://localhost:9090/chio/health
{"status":"healthy","version":"0.1.0","receipt_backend":"ephemeral","revocation_backend":"ephemeral"}

$ curl -fsS http://localhost:9090/chio/live
{"status":"healthy","version":"0.1.0","receipt_backend":"","revocation_backend":""}

# 4. Issue a request through the proxy (needs a capability token).
$ curl -fsS http://localhost:9090/api/search \
    -H "Authorization: Bearer $CAPABILITY_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"query":"hello"}'

# 5. Watch the sidecar log the signed receipt on the way out.
$ docker logs <container> 2>&1 | grep receipt

For an example with an upstream service, see Hello Tool.


Sidecar vs In-Process

PropertyIn-ProcessSidecar
Trust boundaryHost process holds the signing keySidecar container holds the signing key
Latency per kernel callNo IPC. Function call cost.Localhost HTTP. About 100 microseconds.
Independent scalingNo. Kernel scales with the host.Yes. Kernel and host scale separately.
Hot-reload policyNo. Restart the host.Yes. Roll the sidecar revision.
Host languageRust onlyAny language with an HTTP client
Recommended forTrusted single-tenant binariesUntrusted code, multi-tenant, third-party agents

Next Steps