diff --git a/architecture/gateway.md b/architecture/gateway.md index 7646fae99..82df6d65d 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -78,8 +78,20 @@ Remote extension clients share `openshell-extension-core` transport and bearer primitives. When gateway JWT signing is configured, the gateway mints short-lived, exact-audience EdDSA credentials for middleware and interceptors, rotates their in-memory slots without rebuilding clients, and publishes the -public verification key at `/.well-known/jwks.json`. HTTPS extensions can pin -an operator-provided CA while retaining endpoint-hostname verification. +public verification key at `/.well-known/jwks.json` alongside OIDC-shaped +discovery metadata at `/.well-known/openid-configuration`. HTTPS extensions can +pin an operator-provided CA while retaining endpoint-hostname verification. + +Extension credentials reuse the sandbox signing key and are separated from +sandbox-to-gateway admission tokens by exact audience and by an explicit +`typ` of `openshell-ext+jwt`, so a verifier that checks either one alone +cannot confuse the two. A service may advertise `expected_audience` in its +`Describe` manifest; a mismatch against operator configuration fails gateway +startup rather than surfacing as a runtime authentication error. A +registration may opt out of extension authentication entirely with +`allow_insecure_transport`, which permits a plaintext endpoint, attaches no +credential, and warns at every startup. Credential minting is bounded per +sandbox because it resolves the caller's effective policy. Each configured interceptor selects a binding policy. `dynamic` accepts valid manifest declarations and preserves the compatibility behavior. `allowlist` diff --git a/architecture/sandbox.md b/architecture/sandbox.md index 45a4314eb..781e9686a 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -108,6 +108,13 @@ supervisor keeps them in refreshable in-memory slots outside stable middleware configuration, so rotation neither changes `config_revision` nor reconnects the registry. Public custom-CA PEM travels with the stable registration. +The slots live in a supervisor-owned `ExtensionCredentialStore` shared by every +gateway connection the supervisor opens, so the registry's clients and the +polling loop that rotates them observe the same credentials. Configuration +polling runs far more frequently than credentials expire, so the loop rotates +only when a credential is missing or has passed four fifths of its lifetime, +and bounds its sleep by the soonest rotation deadline. + Middleware cannot observe injected credentials or mutate supervisor-owned credential, routing, or framing headers. Body transformations are re-evaluated against body-aware L7 policy before later stages or the upstream can observe diff --git a/docs/extensibility/gateway-interceptors.mdx b/docs/extensibility/gateway-interceptors.mdx index b1f24f3c2..62d97b4c9 100644 --- a/docs/extensibility/gateway-interceptors.mdx +++ b/docs/extensibility/gateway-interceptors.mdx @@ -94,7 +94,13 @@ phases = ["validate"] The gateway supports `http://`, `https://`, and `unix://` interceptor endpoints. When gateway JWT signing is configured, authenticated network interceptors use `https://`; Unix sockets remain available for local integrations. HTTPS uses platform trust roots unless `tls_ca_cert_path` supplies a private CA, and normal hostname verification remains enabled. The gateway calls `Describe` and builds an immutable execution plan during startup. An unavailable service, invalid manifest, missing credential, or unauthorized configured binding prevents the gateway from starting. -The gateway attaches a short-lived EdDSA bearer token to `Describe`, `Evaluate`, and provider-profile snapshot calls. The token uses the configured `audience` (defaulting to `urn:openshell:extension:interceptor:`) and `caller_kind: gateway`. Provision the trusted gateway URL and expected gateway ID separately from token data. The expected issuer is exactly `openshell-gateway:`; JWKS does not establish that identity by itself. Fetch the public signing key from `GET /.well-known/jwks.json` over authenticated TLS at that URL, or provision it through the deployment when the interceptor cannot reach the gateway. Pin `alg` to `EdDSA` and validate `kid`, signature, expected issuer, exact audience, positive expiry, and caller kind. +The gateway attaches a short-lived EdDSA bearer token to `Describe`, `Evaluate`, and provider-profile snapshot calls. The token uses the configured `audience` (defaulting to `urn:openshell:extension:interceptor:`) and `caller_kind: gateway`. + +Return your expected audience in the `expected_audience` field of your `Describe` manifest. The gateway compares it against the configured value and refuses to start on a mismatch, so a misconfigured audience fails at startup instead of returning an opaque 401 on every intercepted call. Leave the field empty to skip the check. + +Provision the trusted gateway URL and expected gateway ID separately from token data. The expected issuer is exactly `openshell-gateway:`; JWKS does not establish that identity by itself. `GET /.well-known/openid-configuration` returns that issuer alongside `jwks_uri`, so an interceptor configured with only the gateway URL can discover both; it is OIDC-shaped rather than OIDC-compliant, because `issuer` is the gateway identity rather than the serving URL. Fetch it over authenticated TLS at the trusted gateway URL, or provision the public signing key through the deployment when the interceptor cannot reach the gateway. Pin `alg` to `EdDSA`, require `typ` to be exactly `openshell-ext+jwt`, and validate `kid`, signature, expected issuer, exact audience, positive expiry, and caller kind. + +Set `allow_insecure_transport = true` on an interceptor to keep a plaintext `http://` endpoint working with no credential attached. The gateway logs a warning naming the interceptor at every startup, and the service cannot distinguish the gateway from any other client that can reach it. Registration is static. Restart the gateway after adding, removing, or changing an interceptor. See [Gateway Configuration](/reference/gateway-config#gateway-interceptors) for the complete field reference. @@ -168,4 +174,5 @@ The gateway emits structured evaluation logs containing the interceptor name, bi - `current_state` is available only in the `validate` contract. The gateway does not yet populate it with method-specific state. - Registration changes require a gateway restart. - mTLS client authentication, service health checks, runtime registration, and overlapping signing-key rotation are not available. +- Extension tokens and sandbox-to-gateway tokens are signed by the same key, separated by audience and `typ`. The extension credential path cannot yet be rotated or revoked independently of sandbox admission. - Interceptors cannot receive or mutate protobuf fields marked secret. diff --git a/docs/extensibility/supervisor-middleware.mdx b/docs/extensibility/supervisor-middleware.mdx index 29b10cd20..4c0e18aa5 100644 --- a/docs/extensibility/supervisor-middleware.mdx +++ b/docs/extensibility/supervisor-middleware.mdx @@ -59,6 +59,7 @@ timeout = "500ms" | `grpc_endpoint` | Service address reachable from both the gateway and sandbox supervisors. Authenticated extensions use TLS `https://`. | | `tls_ca_cert_path` | Optional PEM trust roots for a private HTTPS service. Custom roots replace platform roots and retain hostname verification. | | `audience` | Exact audience expected by the service. Defaults to `urn:openshell:extension:middleware:`. | +| `allow_insecure_transport` | Opt this registration out of extension authentication, permitting a plaintext `http://` endpoint with no bearer credential. Defaults to `false`. Development and trusted-network deployments only. | | `max_body_bytes` | Operator limit applied to every binding exposed by the service, up to the 4 MiB platform maximum. | | `timeout` | Optional service-wide RPC timeout using an integer with an `ms` or `s` suffix. Defaults to `500ms`; valid values range from `10ms` through `30s`. | @@ -72,7 +73,26 @@ Registration is static. Restart the gateway after adding, removing, or changing When gateway JWT signing is configured, OpenShell attaches a short-lived EdDSA bearer token to every remote middleware RPC. Gateway calls use `caller_kind: gateway`; sandbox supervisor calls use `caller_kind: supervisor` and include the sandbox ID. Supervisors request credentials by registration name through `RefreshSandboxToken`. The gateway derives the audience from operator-owned configuration and authorizes each name against the sandbox's effective policy. -Provision the trusted gateway URL and expected gateway ID separately from token data. The expected issuer is exactly `openshell-gateway:`; fetching JWKS does not establish that identity by itself. Obtain the public key from `GET /.well-known/jwks.json` over authenticated TLS at the trusted gateway URL, or provision it through the deployment when the service cannot reach the gateway. Cache keys by `kid`. Pin `alg` to `EdDSA` and validate the signature, expected issuer, exact audience, positive expiry, caller kind, and sandbox identity when required. A sandbox-to-gateway JWT is not an extension credential even though both token types use the same signing key. +Return your expected audience in the `expected_audience` field of your `Describe` manifest. OpenShell compares it against the configured value and refuses to start on a mismatch, so a misconfigured audience fails at startup instead of returning an opaque 401 on every request. Leave the field empty to skip the check. + +Provision the trusted gateway URL and expected gateway ID separately from token data. The expected issuer is exactly `openshell-gateway:`; fetching JWKS does not establish that identity by itself. `GET /.well-known/openid-configuration` returns that issuer alongside `jwks_uri`, so a service configured with only the gateway URL can discover both. The document is OIDC-shaped rather than OIDC-compliant: `issuer` is the gateway identity, not the URL serving the document, so compare `iss` against that value and do not infer trust from the document's location. Fetch it over authenticated TLS at the trusted gateway URL, or provision the public key through the deployment when the service cannot reach the gateway. + +Cache keys by `kid`. Validate, at minimum: + +- `typ` is exactly `openshell-ext+jwt`. Extension tokens and sandbox-to-gateway bootstrap tokens share a signing key and differ only in audience; this header is a second, independent discriminator. +- `alg` is pinned to `EdDSA`. Never select the algorithm from the token. +- Signature, expected issuer, exact audience, and positive expiry. +- `caller_kind`, and the sandbox identity when your service scopes behavior per sandbox. + +A sandbox-to-gateway JWT is not an extension credential even though both token types use the same signing key. + +Each token carries a unique `jti`. OpenShell does not track it. If your service needs replay resistance beyond the token lifetime, keep a bounded cache of recently seen `jti` values covering at least the maximum token lifetime of one hour and reject repeats. + +### Run Without Extension Authentication + +Set `allow_insecure_transport = true` on a registration to keep a plaintext `http://` endpoint working. OpenShell then attaches no credential to that service, supervisors do not request one, and the gateway refuses to mint one if asked. The gateway logs a warning naming the registration at every startup. + +The service cannot distinguish OpenShell from any other client that can reach it. Use this only where the network already provides that guarantee, and prefer `https://` everywhere else. ## Apply Middleware with Policy @@ -185,5 +205,7 @@ See [Logging](/observability/logging) for log access and [OCSF JSON Export](/obs - The typed operation and phase are `HTTP_REQUEST/PRE_CREDENTIALS`. - Selection uses destination host include and exclude patterns. - A fail-closed middleware cannot cover `tls: skip` endpoints because OpenShell cannot inspect that traffic. An all-`fail_open` match may cover the endpoint; OpenShell bypasses the middleware and emits a detection finding. -- Operator-run services use TLS `https://` when gateway JWT signing is enabled. Certificates must chain to the configured custom CA or platform roots, and the endpoint hostname must match. +- Operator-run services use TLS `https://` when gateway JWT signing is enabled, unless the registration sets `allow_insecure_transport`. Certificates must chain to the configured custom CA or platform roots, and the endpoint hostname must match. +- Extension tokens and sandbox-to-gateway tokens are signed by the same key. They are separated by audience and by `typ`, but the extension credential path cannot yet be rotated or revoked independently of sandbox admission. +- OpenShell does not track `jti`; replay resistance within a token's lifetime is the service's responsibility. - mTLS client authentication, health checks, runtime registration, and overlapping signing-key rotation are not available. diff --git a/docs/reference/gateway-config.mdx b/docs/reference/gateway-config.mdx index 7af061177..94cb1dbbd 100644 --- a/docs/reference/gateway-config.mdx +++ b/docs/reference/gateway-config.mdx @@ -273,7 +273,9 @@ The gateway connects to every registered service and validates `Describe` before `timeout` is the operator-configured service-wide RPC timeout. It accepts the same compact duration syntax as gateway interceptors: an integer followed by `ms` or `s`, such as `500ms` or `2s`. Values must be between `10ms` and `30s`, inclusive. Omit the field to use the 500 ms platform default. A binding may advertise a shorter `timeout` in the `Describe` manifest, but it cannot extend the operator-configured deadline; OpenShell uses the smaller value. OpenShell validates both levels before accepting the service. The effective timeout covers `ValidateConfig` and `EvaluateHttpRequest`; `Describe` uses the service timeout because binding metadata is not available yet. -The service `grpc_endpoint` supports plaintext `http://` on legacy gateways without JWT signing and TLS `https://` for authenticated extensions. HTTPS uses the platform trust store unless `tls_ca_cert_path` names a certificate-only PEM bundle. OpenShell rejects bundles containing private keys, loads the certificates at gateway startup, and distributes only public certificates to sandbox supervisors; normal TLS hostname verification still applies. `audience` sets the exact audience for gateway-minted service tokens and defaults to `urn:openshell:extension:middleware:`. When `gateway_jwt` is configured, OpenShell requires HTTPS and attaches short-lived bearer credentials to gateway and supervisor calls. mTLS client authentication, health checks, and runtime registration are not currently supported. The endpoint must be reachable from both the gateway and sandbox supervisors; use `host.openshell.internal` or another shared address that can be resolved in both places. +The service `grpc_endpoint` supports plaintext `http://` and TLS `https://`. HTTPS uses the platform trust store unless `tls_ca_cert_path` names a certificate-only PEM bundle. OpenShell rejects bundles containing private keys, loads the certificates at gateway startup, and distributes only public certificates to sandbox supervisors; normal TLS hostname verification still applies. `audience` sets the exact audience for gateway-minted service tokens and defaults to `urn:openshell:extension:middleware:`; when the service advertises `expected_audience` in its `Describe` manifest, OpenShell refuses to start on a mismatch. + +When `gateway_jwt` is configured, OpenShell attaches short-lived bearer credentials to gateway and supervisor calls and requires `https://`. A middleware endpoint must be reachable from sandbox supervisors, so Unix sockets are not an option here. Set `allow_insecure_transport = true` on a registration to keep a plaintext `http://` endpoint: OpenShell then attaches no credential, supervisors do not request one, and the gateway logs a warning naming the registration at every startup. mTLS client authentication, health checks, and runtime registration are not currently supported. The endpoint must be reachable from both the gateway and sandbox supervisors; use `host.openshell.internal` or another shared address that can be resolved in both places. See [Supervisor Middleware](/extensibility/supervisor-middleware) for selection, failure, body-limit, and operational guidance. @@ -281,7 +283,18 @@ See [Supervisor Middleware](/extensibility/supervisor-middleware) for selection, `[[openshell.gateway.interceptors]]` configures gateway-side interceptor services. The gateway calls each service's `Describe` RPC at startup, validates its declared OpenShell RPC bindings against the compiled service descriptor, and applies matching phases from a central gRPC middleware path. Interceptors can target only methods in the gateway's built-in allowlist of unary mutation RPCs. New RPCs are non-interceptable until they are deliberately added to that allowlist; adding one does not require handler-specific interceptor code. Request bodies are exposed as protobuf JSON objects. Fields marked secret in the protobuf schema are recursively omitted from requests and post-commit responses. Interceptors cannot patch an omitted field or a containing object. -HTTPS interceptor endpoints use the platform trust store by default. Set `tls_ca_cert_path` to a PEM certificate bundle for a private CA; normal TLS hostname verification still applies. `audience` sets the exact audience for gateway-minted service tokens and defaults to `urn:openshell:extension:interceptor:`. When `gateway_jwt` is configured, network interceptors must use HTTPS and receive short-lived gateway-caller bearer credentials; local Unix sockets are also supported. +HTTPS interceptor endpoints use the platform trust store by default. Set `tls_ca_cert_path` to a PEM certificate bundle for a private CA; normal TLS hostname verification still applies. `audience` sets the exact audience for gateway-minted service tokens and defaults to `urn:openshell:extension:interceptor:`; when the interceptor advertises `expected_audience` in its `Describe` manifest, the gateway refuses to start on a mismatch. When `gateway_jwt` is configured, network interceptors must use HTTPS and receive short-lived gateway-caller bearer credentials; local Unix sockets are also supported. Set `allow_insecure_transport = true` to keep a plaintext `http://` interceptor endpoint with no credential attached and a startup warning. + +### Extension Token Verification Endpoints + +When `gateway_jwt` is configured the gateway publishes two unauthenticated documents that extension services use to verify OpenShell callers: + +| Path | Contents | +| --- | --- | +| `/.well-known/jwks.json` | Single-key JWKS holding the Ed25519 public key and its `kid`. | +| `/.well-known/openid-configuration` | OIDC-shaped discovery metadata: the exact expected `iss`, an absolute `jwks_uri`, and `EdDSA` as the only supported signing algorithm. | + +The discovery document is OIDC-shaped rather than OIDC-compliant: `issuer` is the gateway identity (`openshell-gateway:`), not the URL the document is served from. Verifiers compare `iss` against that value and must not infer trust from the document's location; the serving TLS connection is what authenticates the gateway. Both endpoints return `404` when `gateway_jwt` is not configured. `binding_policy` controls how the manifest and operator binding configuration combine: diff --git a/rfc/0009-supervisor-middleware/README.md b/rfc/0009-supervisor-middleware/README.md index f812d0e47..f43e05ed7 100644 --- a/rfc/0009-supervisor-middleware/README.md +++ b/rfc/0009-supervisor-middleware/README.md @@ -328,13 +328,13 @@ grpc_endpoint = "https://middleware.example.internal:443" max_body_bytes = 1048576 ``` -The stable transport requirement is confidentiality plus authentication of the intended middleware service. The alpha mechanism uses HTTPS with platform roots or an operator-provided CA and retains normal hostname verification. OpenShell authenticates gateway and supervisor calls with short-lived, exact-audience Ed25519 JWTs. Supervisors obtain only policy-selected service credentials through `RefreshSandboxToken`; services verify the public key through gateway JWKS or operator provisioning. mTLS and overlapping signing-key rotation remain follow-up hardening (see [appendices/protocol-extensions.md](appendices/protocol-extensions.md#middleware-authentication)). +The stable transport requirement is confidentiality plus authentication of the intended middleware service. Phase 1 may temporarily accept a plaintext `http://` endpoint only when the same entry explicitly sets `allow_insecure = true`. OpenShell rejects plaintext without that opt-in, warns prominently, and records the insecure registration as auditable configuration state. This escape hatch is limited to trusted local development and isolated research environments. Phase 2 removes plaintext support and the `allow_insecure` field, requiring authenticated encrypted transport. That removal is an intentional research-preview breaking change with no long-term compatibility obligation. The exact phase 2 mechanism, such as mTLS or TLS plus explicit caller authentication, is follow-up protocol work (see [appendices/protocol-extensions.md](appendices/protocol-extensions.md#middleware-authentication)). For each binding, the operator's `max_body_bytes` must not exceed the binding capability returned by `Describe` or the 4 MiB platform maximum. The gateway rejects an invalid registration rather than silently clamping it. The resulting operator limit applies to every binding exposed by that registration. RPC timeouts use an integer with an `ms` or `s` suffix, range from 10 ms through 30 s, and default to 500 ms. A binding may advertise its own timeout through `Describe`; that value overrides the service registration timeout. The service timeout applies to `Describe`, while the effective binding timeout applies to `ValidateConfig` and `EvaluateHttpRequest`. -The external-service endpoint is trusted operator infrastructure in v1. Both directions are explicit: TLS and the configured trust roots authenticate the middleware service, while the exact-audience JWT proves that a gateway or policy-authorized sandbox supervisor made the call. The middleware validates issuer, audience, expiry, caller kind, and sandbox identity where applicable. +The external-service endpoint is trusted operator infrastructure in v1. The auth design must make both directions explicit: the supervisor proves to the middleware that the call is authorized for the specific middleware identity, and the supervisor verifies it is calling the intended middleware service. Binding IDs may be bare (`anonymizer`) or namespaced with `/` (`nvidia/anonymizer`, `acme/security/pii-redactor`). Empty path segments are invalid, so `/foo`, `foo/`, and `foo//bar` are rejected. The `openshell/` namespace is reserved for built-in OpenShell middleware, such as `openshell/regex` or `openshell/sigv4`. Policy config map keys remain stable local identities for metadata namespacing and diagnostics; the `middleware` field selects the binding. @@ -515,7 +515,7 @@ This section closes the current review themes. ### Explicit deferrals - **Provider-profile middleware.** V1 middleware configs live in sandbox policy, not provider profiles. Provider-supplied network policies can be targeted after effective policy assembly. Provider-profile opt-ins for built-in middleware such as `openshell/sigv4`, and reusable cross-sandbox middleware profiles, are follow-up design work. -- **Authenticated transport hardening.** Alpha uses custom-CA-capable TLS plus gateway-signed bearer JWTs and single-key JWKS. mTLS, replay resistance beyond short expiry, and overlapping key rotation remain follow-up work. +- **Authenticated transport mechanism.** Phase 2 requires authenticated encrypted transport. The exact choice between mTLS, TLS plus caller authentication, or an equivalent mechanism, including credential delivery and rotation, is follow-up protocol work. - **Health checks.** V1 relies on connection establishment, `Describe`, per-request invocation, timeout, `on_error`, and registry polling. A dedicated health RPC can improve alerting later but is not required for correctness. - **Registration ergonomics and ownership.** V1 middleware registration is an operator concern: middleware services are declared in gateway configuration and changing the registered set requires a gateway restart. Runtime user-managed registration, CLI/API helpers, SDK helpers, and an agent skill for scaffolding or registering middleware are useful follow-ups after the policy and service contract stabilize. - **Post-call budget reconciliation.** Budget-style middleware that needs final route/model, status, content length, or token usage needs a metadata-only hook such as `HttpResponse/completed`. That hook is listed as a future extension and is not part of the v1 request hook. diff --git a/rfc/0009-supervisor-middleware/appendices/extension-authentication.md b/rfc/0009-supervisor-middleware/appendices/extension-authentication.md new file mode 100644 index 000000000..1d0c31957 --- /dev/null +++ b/rfc/0009-supervisor-middleware/appendices/extension-authentication.md @@ -0,0 +1,64 @@ +# Appendix: Extension Authentication (Alpha) + +> This is an appendix to the [RFC](../README.md). Please familiarize yourself with the RFC before reading this. + +The RFC body left the authenticated-transport mechanism as follow-up protocol work: it required confidentiality plus authentication of the intended middleware service, described a phase 1 `allow_insecure` escape hatch, and deferred the phase 2 choice between mTLS, TLS plus explicit caller authentication, or an equivalent. This appendix records the mechanism that was actually built for alpha. It supersedes the body's transport-authentication and `allow_insecure` paragraphs; the rest of the body is unchanged. + +Related: [protocol-extensions.md](protocol-extensions.md#middleware-authentication). + +## What ships in alpha + +Transport is HTTPS with either platform trust roots or an operator-provided CA bundle, with normal certificate and endpoint-hostname verification. A middleware endpoint must be reachable from every sandbox supervisor as well as the gateway, so a gateway-local Unix socket is not an option for this mechanism. + +Caller identity is a short-lived Ed25519 JWT minted by the gateway's existing sandbox signing authority. The gateway attaches one to its own `Describe` and `ValidateConfig` calls; sandbox supervisors attach one to `Describe` and `EvaluateHttpRequest`. Both directions of the RFC's stated requirement are covered: TLS and the configured trust roots authenticate the middleware service to OpenShell, and the exact-audience JWT proves to the middleware that a gateway or a policy-authorized sandbox supervisor made the call. + +## Claim contract + +| Claim | Meaning | +|---|---| +| `iss` | Exactly `openshell-gateway:`. | +| `aud` | The exact audience for one registration. Never a list. | +| `sub` | Gateway identity for gateway callers; `spiffe://openshell/sandbox/` for supervisor callers. | +| `caller_kind` | `gateway` or `supervisor`. | +| `sandbox_id` | Present only for supervisor callers. | +| `jti` | Unique per token. OpenShell does not track it. | +| `iat`, `exp` | Bounded lifetime, at most one hour. | + +The JOSE header carries `alg: EdDSA`, the signing `kid`, and `typ: openshell-ext+jwt`. + +Explicit typing exists because extension tokens and sandbox-to-gateway admission tokens are signed by the same key and would otherwise be separated by audience alone. A verifier that requires this `typ` cannot accept a sandbox bootstrap credential even if it neglects to check `aud`. This is defense in depth for the most likely verifier mistake, not a replacement for audience validation. + +## Authorization and distribution + +Supervisors request credentials by operator registration name through the existing `RefreshSandboxToken` RPC, which already authenticates a sandbox principal. The gateway resolves each requested name against server-owned registration metadata and the sandbox's effective policy before minting; unknown, unselected, or duplicated names are rejected. Callers never choose an audience. + +Because that resolution runs the full effective-policy composition, two bounds apply. Supervisors rotate only when a credential is missing or has passed four fifths of its lifetime rather than on every configuration poll, and the gateway caps how many minting requests a single sandbox may make per minute. + +## Audience agreement + +The audience is operator configuration on the OpenShell side and service configuration on the middleware side. A mismatch is otherwise invisible until it produces an authentication failure on every evaluated request. + +A service may therefore advertise the audience it verifies in the `expected_audience` field of its `Describe` manifest. OpenShell compares that value against the configured one and rejects the registration at startup on a mismatch. An empty field means the service does not advertise an audience and the check is skipped, so this is additive for existing services. + +## Verification key distribution + +The gateway publishes its public signing key as a single-key JWKS at `/.well-known/jwks.json`, and OIDC-shaped discovery metadata at `/.well-known/openid-configuration` carrying the expected `iss`, an absolute `jwks_uri`, and `EdDSA` as the only supported algorithm. + +The discovery document is OIDC-shaped rather than OIDC-compliant, in the same way and for the same reason as the equivalent Kubernetes endpoint: `issuer` is the gateway identity, not the URL the document is served from. Verifiers compare `iss` against that value. Fetching the document does not establish gateway identity; the serving TLS connection does. Deployments where the service cannot reach the gateway provision the public key out of band instead. + +## Compatibility + +A registration may set `allow_insecure_transport = true`, which replaces the body's `allow_insecure` field with a clearer name and a wider meaning: it opts the registration out of extension authentication entirely. OpenShell attaches no credential, supervisors do not request one, the gateway refuses to mint one if asked, and a warning naming the registration is logged at every gateway startup. + +This keeps plaintext registrations working for trusted local development and isolated research environments, and preserves compatibility for deployments configured before extension authentication existed. The body's intent that this be a temporary, auditable, prominently-warned escape hatch is retained. + +## Residual risks + +- **Bearer replay within the token lifetime.** Anything that obtains a token can use it until expiry. Short bounded lifetimes limit the window; `jti` is present so a service can add a bounded-window replay cache, but OpenShell does not track it. +- **Shared signing key across two trust domains.** Extension credentials and sandbox admission credentials come from one key and one `kid`. They are separated by audience and `typ`, but extension-token issuance cannot be rotated or revoked independently of sandbox admission. A separate extension signing key is the natural next step and the `kid`-based design does not preclude it. +- **Single-key JWKS.** There is no overlap window, so key rotation is not yet a zero-downtime operation. +- **No channel binding.** mTLS or another proof-of-possession mechanism remains deferred hardening. + +## Deferred + +mTLS client authentication, multi-key rotation with an overlap window, replay resistance beyond short expiry, and per-runtime secret delivery are all out of scope for alpha and remain follow-up work. diff --git a/rfc/0009-supervisor-middleware/appendices/protocol-extensions.md b/rfc/0009-supervisor-middleware/appendices/protocol-extensions.md index 924c7bd69..340ff38df 100644 --- a/rfc/0009-supervisor-middleware/appendices/protocol-extensions.md +++ b/rfc/0009-supervisor-middleware/appendices/protocol-extensions.md @@ -83,4 +83,6 @@ Phase 2 removes plaintext endpoint support and removes `allow_insecure`. Every e The exact phase 2 mechanism is deferred. Follow-up protocol work should choose and specify mTLS, TLS plus explicit caller authentication, or an equivalent design, including trust roots, client identity, credential delivery, certificate or key rotation, middleware identity binding, and how supervisors receive authentication material. +The alpha mechanism that was subsequently built - TLS with optional operator-provided trust roots plus short-lived, exact-audience gateway-signed JWTs - is recorded in [extension-authentication.md](extension-authentication.md). It supersedes this section's `allow_insecure` design with `allow_insecure_transport` and narrows, but does not close, the phase 2 question: mTLS and overlapping key rotation remain deferred. + Even during the phase 1 plaintext exception, the hook stays before provider credential injection, and OpenShell does not forward original `Authorization`, `Cookie`, or other protected headers to middleware. This preserves the separation between content inspection and upstream credential injection while authenticated transport is completed. diff --git a/rfc/0010-gateway-interceptors/README.md b/rfc/0010-gateway-interceptors/README.md index 946150cb2..b49d39ead 100644 --- a/rfc/0010-gateway-interceptors/README.md +++ b/rfc/0010-gateway-interceptors/README.md @@ -273,12 +273,9 @@ The framework uses one protobuf/gRPC service contract. Gateway interceptor endpoints connect over gRPC, either to a remote endpoint or over a Unix domain socket. -Gateway interceptor connections use short-lived, exact-audience bearer JWTs -minted by the gateway's existing Ed25519 signing authority. Integrations verify -the gateway key through its JWKS or an operator-provisioned public key and -validate issuer, audience, expiry, and `caller_kind: gateway`. HTTPS supports -an operator-provided CA with hostname verification. mTLS and overlapping key -rotation remain deferred hardening. +All gateway interceptor connections require authentication. The exact +authentication model is out of scope for this RFC, but implementations should +support mTLS and bearer-token authentication. ### Selection and ordering diff --git a/rfc/0010-gateway-interceptors/appendices/extension-authentication.md b/rfc/0010-gateway-interceptors/appendices/extension-authentication.md new file mode 100644 index 000000000..0e3d404f2 --- /dev/null +++ b/rfc/0010-gateway-interceptors/appendices/extension-authentication.md @@ -0,0 +1,31 @@ +# Appendix: Extension Authentication (Alpha) + +> This is an appendix to the [RFC](../README.md). Please familiarize yourself with the RFC before reading this. + +The RFC body states that all gateway interceptor connections require authentication, leaves the model out of scope, and suggests implementations should support mTLS and bearer-token authentication. This appendix records the mechanism that was actually built for alpha. It supersedes that paragraph; the rest of the body is unchanged. + +Gateway interceptors and supervisor middleware share one implementation. The full claim contract, authorization model, key distribution, and residual risks are documented once in [RFC 0009's appendix](../../0009-supervisor-middleware/appendices/extension-authentication.md). This appendix records only what differs for interceptors. + +## What ships in alpha + +The bearer-token half of the body's suggestion, not the mTLS half. The gateway attaches a short-lived, exact-audience Ed25519 JWT to `Describe`, `Evaluate`, and provider-profile snapshot calls. mTLS remains deferred hardening. + +Interceptors are called only by the gateway, so every token carries `caller_kind: gateway` and no `sandbox_id`. There is no supervisor-side distribution path and no policy-based authorization step: the gateway mints its own credentials at startup from configuration and rotates them in place. + +## Transport + +Unlike middleware, interceptors keep the body's Unix domain socket option. A gateway-local socket is reachable by the only caller that exists, so both `https://` and `unix://` are accepted when gateway JWT signing is configured. HTTPS endpoints may pin an operator-provided CA bundle and retain normal hostname verification. + +## Audience agreement + +The audience defaults to `urn:openshell:extension:interceptor:` and may be set explicitly per interceptor. An interceptor may advertise the audience it verifies in the `expected_audience` field of its `Describe` manifest; the gateway compares it against the configured value and refuses to start on a mismatch, turning a silent runtime authentication failure into a startup failure. + +Because the body already makes an unavailable service, invalid manifest, or unauthorized binding a startup failure, this fits the existing posture: interceptor configuration problems surface before the gateway serves traffic. + +## Compatibility + +An interceptor may set `allow_insecure_transport = true` to keep a plaintext `http://` endpoint with no credential attached. The gateway logs a warning naming the interceptor at every startup. This exists for local development and for deployments configured before extension authentication existed. + +## Deferred + +mTLS client authentication, multi-key rotation with an overlap window, and replay resistance beyond short expiry remain follow-up work, as recorded in RFC 0009's appendix.