Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
with:
toolchain: 1.97.1
toolchain: 1.99.0
components: rustfmt, clippy
- uses: Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc # v2.9.1
- run: cargo fmt --all -- --check
Expand All @@ -64,7 +64,7 @@ jobs:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
with:
toolchain: 1.97.1
toolchain: 1.99.0
- uses: Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc # v2.9.1
- name: Install pinned cargo-deny
run: cargo install cargo-deny --version 0.20.2 --locked
Expand Down
28 changes: 14 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "charon"
version = "0.1.0"
edition = "2024"
rust-version = "1.97"
rust-version = "1.99"
description = "Credential-injecting forward proxy for AI agents and other workloads"
license = "MIT"
repository = "https://github.com/ak5/charon"
Expand All @@ -24,7 +24,7 @@ hyper-util = { version = "0.1", features = ["tokio"] }
rcgen = { version = "0.14", features = ["pem", "x509-parser"] }
reqwest = { version = "0.13", default-features = false, features = ["http2", "rustls", "stream"] }
rusqlite = { version = "0.38", features = ["bundled"] }
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
rustls = { version = "0.23.45", default-features = false, features = ["ring", "std", "tls12"] }
secrecy = "0.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM docker.io/library/rust:1.97.1-bookworm@sha256:0e2bcaef56d041a486784e54104a81aebe0da44bd03019bd70bc0401e42e4a97 AS build
FROM docker.io/library/rust:1.99.0-bookworm@sha256:59037199c44290f2befcdd58dcc540164763fc296950255aaefeef096a1866b0 AS build
WORKDIR /src
COPY Cargo.toml Cargo.lock ./
COPY src ./src
Expand Down
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<p align="center">
<a href="https://github.com/ak5/charon/actions/workflows/ci.yml"><img src="https://github.com/ak5/charon/actions/workflows/ci.yml/badge.svg" alt="CI status"></a>
<a href="https://github.com/ak5/charon/pkgs/container/charon"><img src="https://img.shields.io/badge/container-ghcr.io%2Fak5%2Fcharon-blue" alt="Container image"></a>
<a href="https://github.com/ak5/charon/blob/main/Cargo.toml"><img src="https://img.shields.io/badge/rust-1.97%2B-orange" alt="Rust 1.97 or newer"></a>
<a href="https://github.com/ak5/charon/blob/main/Cargo.toml"><img src="https://img.shields.io/badge/rust-1.99%2B-orange" alt="Rust 1.99 or newer"></a>
<a href="https://github.com/ak5/charon/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ak5/charon" alt="MIT license"></a>
</p>

Expand All @@ -16,7 +16,8 @@
</p>

Charon is a transparent gateway and forward proxy that adds credentials to
approved outbound requests.
approved outbound requests. It also forwards secretless and caller-authenticated
traffic through an exclusive workload gateway for ordinary proxy clients.
It lets a workload call an API without putting the API credential in that
workload's environment, filesystem, or container image.

Expand All @@ -25,7 +26,7 @@ narrowly scoped access to an authenticated API without placing the long-lived
credential inside the agent runtime. The same model works for CLIs, builds,
development containers, and other programs.

The workload sends an opaque capability reference instead of a real credential. Charon
For credential mediation, the workload sends an opaque capability reference instead of a real credential. Charon
checks authorization, matches the request against local policy, obtains the
credential from the configured secret store, and hydrates the reference only in
the request sent upstream.
Expand All @@ -51,13 +52,13 @@ anything that compromises it.
Charon moves the credential into a smaller, separately operated process. A
request is allowed only when all of these agree:

- a signed, short-lived, single-use workload manifest in explicit-proxy mode,
or an Infra-isolated listener bound to one workload in transparent mode;
- a signed, short-lived, single-use manifest in signed proxy mode, or an
Infra-isolated listener bound to one workload in transparent/exclusive mode;
- a named capability in Charon's configuration; and
- the actual destination hostname, HTTP method, and path.

The workload cannot choose a secret, a secret-store item, or an unconfigured
destination. Charon does not return credentials to workloads and does not
destination. Charon does not return brokered credentials to workloads and does not
follow redirects after adding one.

Applications keep using their ordinary credential settings. The configured
Expand Down Expand Up @@ -91,7 +92,7 @@ identifiers supplied by the issuer. They are integration context, not secret
selectors or core Charon concepts. This part of the public contract is under
review before 1.0.

## How a request works
## How a signed credential request works

1. A trusted issuer gives the workload a signed manifest for a named
capability.
Expand All @@ -118,7 +119,7 @@ the Infra routing boundary. Charon's ordinary health endpoints are described by

## Development

The project requires Rust 1.97 or newer. [mise](https://mise.jdx.dev/) is
The project requires Rust 1.99 or newer. [mise](https://mise.jdx.dev/) is
optional; it installs the pinned toolchain and provides short names for common
development commands.

Expand Down Expand Up @@ -240,3 +241,12 @@ mise run check
## License

Charon is licensed under the [MIT License](LICENSE).

## Exclusive Hermes network gateway

The [exclusive workload gateway](contracts/workload-gateway.md) provides a separate
`--gateway-config` mode for ordinary HTTP(S) proxy clients, including reusable
CONNECT tunnels. It binds one fixed isolated workload, supports secretless
forwarding and typed credential sinks, and requires its own validated schema.
Signed proxy authentication, transparent service lanes, and Hermes tool
admission remain independent controls. Infra owns isolation, trust and cutover.
9 changes: 9 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,12 @@ the [`approval broker operator runbook`](../docs/approval-broker-operations.md).

Tool admission and receipt contracts are also implemented outside Charon.
Their first adapter is [`charon-hermes`](../integrations/hermes/README.md).

## Exclusive Hermes network gateway

The [exclusive workload gateway](workload-gateway.md) provides a separate
`--gateway-config` mode for ordinary HTTP(S) proxy clients, including reusable
CONNECT tunnels. It binds one fixed isolated workload, supports secretless
forwarding and typed credential sinks, and requires its own validated schema.
Signed proxy authentication, transparent service lanes, and Hermes tool
admission remain independent controls. Infra owns isolation, trust and cutover.
9 changes: 9 additions & 0 deletions contracts/forward-proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,12 @@ Charon has no request-path callback to an issuer or application database. It
exposes no workload API to create, rotate, revoke, enumerate, or choose secrets,
providers, realms, or policy. Realm lifecycle objects are separate
operator/reconciler contracts.

## Exclusive Hermes network gateway

The [exclusive workload gateway](workload-gateway.md) provides a separate
`--gateway-config` mode for ordinary HTTP(S) proxy clients, including reusable
CONNECT tunnels. It binds one fixed isolated workload, supports secretless
forwarding and typed credential sinks, and requires its own validated schema.
Signed proxy authentication, transparent service lanes, and Hermes tool
admission remain independent controls. Infra owns isolation, trust and cutover.
139 changes: 139 additions & 0 deletions contracts/workload-gateway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Exclusive workload explicit proxy contract

Version: 1

Start `charon --gateway-config <path>` with a complete policy conforming to
`GatewayConfig` in `src/gateway.rs`. Validate it with
`charon gateway validate <path>` before enabling routing. Unknown fields and
unsupported versions fail closed. The exact synthetic deployment example is
[`examples/hermes-gateway.toml`](../examples/hermes-gateway.toml).

This is a separate process/listener mode from `--config`. The signed explicit
proxy continues to require a fresh signed single-use manifest. Transparent
service listeners continue to bind one service. Neither becomes an anonymous
shared proxy. Hermes semantic tool admission is independent and unchanged.

## Identity and transport

One listener binds one fixed `realm` and `workload`. `exclusive_network = true`
is a mandatory operator acknowledgement, not proof of isolation. Infra must
make the listener reachable only from that workload, block all direct remote
TCP egress, prevent access from other workloads/host namespaces, and deny IPv6,
UDP/443, QUIC, and alternate routes. Source IP, URL, header, or capability text
is not authentication. A shared listener is prohibited. Charon never queries
an orchestrator or application database.

Ordinary clients use an HTTP proxy URL without proxy authentication. HTTPS
requires `CONNECT exact-host:443`, TLS interception, and origin-form HTTP/1.1
or HTTP/2. CONNECT, TLS SNI, HTTP `Host`, and HTTP/2 `:authority` must agree;
conflicting/malformed/duplicate authority forms fail closed. All permitted
HTTPS terminates at Charon; no splice, direct tunnel, or TLS fallback exists.
Every request on a reused or multiplexed connection is authorized afresh.
CONNECT itself grants no operation or credential. Tunnels expire after one
hour; each operation has its own shorter configured time limits.

The runtime validates upstream certificates with platform trust and pins
public IPv4 DNS addresses for the process lifetime. Literal IP routes, private,
loopback, link-local, metadata, shared-address, benchmark, multicast, reserved,
and IPv6 destinations are unavailable. Authorized DNS is checked before any
provider lookup and the HTTP connector uses the same pin. Ambient proxy
variables cannot change Charon's upstream route. No upstream intermediary is
configured in this mode. Infra owns DNS, public CA trust, and egress isolation.

Secretless HTTP is available only through an explicit `scheme = "http"` route
on port 80; credential mediation requires HTTPS/443. The Hermes example grants
no plaintext route. Nonstandard ports, nested CONNECT, upgrades, HTTP/3, raw
TCP protocols, and WebSockets are denied. Clients receive a fixed denial;
TLS/CONNECT failures close the connection without upstream diagnostics.

## Operation grants and credential ownership

`routes` grant an exact DNS host, scheme, methods, exact `paths` and/or one
literal slash-terminated `path_prefix`, query permission, credential/session
header permission, mediation type, and stream limits. No hostname glob exists.
An explicit `/` prefix grants that named host's API surface; it does not grant
another destination. Overlapping grants for a host/scheme/method fail validation.
Encoded path separators, controls, traversal, URL user information, and routing
ambiguities fail before lookup. Query values are forwarded only when allowed
and are never recorded. Redirects are returned without following them; every
caller-followed hop must pass a new request authorization.

`mediation.kind = "forward"` performs **no secret-provider lookup or health
call**. Tokens in OAuth bodies, Telegram URL paths, and allowed caller-owned
Authorization/cookies remain client-owned. Ordinary API requests need no
capability placeholder. Explicit `caller_headers` permits the closed set
Authorization, Cookie, X-Api-Key, Api-Key, X-Goog-Api-Key, X-Auth-Token, and
X-Session-Token. Supplying these outside its grant is denied. Other application
headers (for example ChatGPT-Account-Id and SDK version headers) pass through;
hop-by-hop, framing, Host and proxy authentication do not.

Credential grants are closed typed sinks:

- `header`: Authorization or a named credential header from that set excluding
Cookie, a policy-owned `secret_ref`, and a value template containing exactly
one `{secret}`. Supply `{{charon.<route-name>}}` at that exact sink, rendered
with the template (for example `Bearer {{charon.github-user}}`).
- `basic`: fixed `username` and policy-owned `secret_ref`. Supply standard Basic
auth with password `{{charon.<route-name>}}`, usable by ordinary curl/Git
credential configuration.

Authorization, framing, caller-header rules, capability sink presence, and
public address checks complete before resolution. A client cannot choose a
provider reference, inject into routing/framing, or ask for arbitrary string
replacement. There is no global body/path replacement. Other typed sinks
remain available through the signed/transparent contracts; this mode does not
pretend to support them. Credential routes accept only credentials of 8–16384
bytes. Provider failures return data-free denials.

## Streaming and sessions

Uploads and downloads use backpressure, cancellation, byte counts, total time
and idle bounds. The example allows 256 MiB uploads and 1 GiB downloads; these
are policy choices and do not change other listener modes. Unknown-length
uploads can reach the origin partially before an overflow; streaming cannot
retract a remote side effect. Total operation time includes provider/DNS work,
upstream headers and upload. Body streams stop on overflow, idle timeout, total
timeout, dependency error, or disconnect. Bodies are not buffered to completion.
SSE/model responses stream through rolling byte redaction, without semantic
prompt/body inspection. Binary downloads retain their bytes on secretless
routes. Credential routes redact known raw/rendered credentials and URL/base64
encodings even across chunk boundaries. This can alter an opaque payload that
contains a brokered credential; it is intentional fail-safe mediation.

HTTP/1.1 keepalive and HTTP/2 multiplexing are supported downstream; reqwest
negotiates HTTP/1.1 or HTTP/2 upstream with certificate validation. Compression
is identity-only: request Accept-Encoding is set to identity, encoded uploads
and non-identity encoded responses fail closed. Clients must handle the fixed
403 for unsupported compression or protocol. No compressed bytes silently
bypass redaction. WebSocket/101 upgrades fail closed.

`session_response_headers` can retain Set-Cookie, WWW-Authenticate,
Authentication-Info, and X-Session-Token on caller-owned routes. They are not
globally removed. Every retained header is checked for known brokered secret
material. Credentials reflected in Location or another retained header deny
the response. A provider credential cannot be returned as a session value.
Charon is not a sanitizer for secrets already owned by the caller, including
OAuth token responses; forwarding those to their owner is necessary behavior.

## Receipts, logs, and verification

The required receipt journal reserves capacity before any operation; failure
denies execution before lookup. Accepted and denied operations contain only
fixed realm/workload, policy capability/service, authorized destination/method,
status, counts, elapsed time, and outcome. Its existing `path` field contains
`/capability/<route-name>` (or `/denied`), **never a request URL/path/query**.
Preauthorization denials use fixed `denied` identifiers. Authority/handshake
failures have fixed metadata log outcomes, without a request receipt.
Streaming completion/cancellation uses the existing hash-chained journal.

Errors and logs omit headers, tokens, URLs, prompts, bodies and provider
references. Gateway commands enforce `off,charon=info` regardless of RUST_LOG
so dependency transport debug logs cannot expose traffic. Embedders must apply
the same logging restriction. The interception CA/key, provider credentials,
and decrypted traffic remain within the trusted Charon boundary.

`/healthz` and `/readyz` return 204 for process and journal availability; neither
proves mediation, provider readiness, isolation, or client trust. Run the real
TLS fixture suite (`mise exec -- cargo test gateway --lib`) and the deployment
verification procedure in [`docs/hermes-gateway.md`](../docs/hermes-gateway.md).
Never remove an Infra convergence guard based on generic liveness.
Loading
Loading