Files
OpenShell/proto/gateway_interceptor.proto
Piotr Mlocek bdabb54cb3 fix(security): authenticate extension services (#2638)
* fix(security): authenticate extension services

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

* feat(extension-core): verify gateway JWTs

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

* refactor(extension-core): keep inbound verification external

This should become an extension SDK package.

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

* fix(security): harden the extension authentication contract

Follow-up hardening on the alpha extension authentication mechanism.

Claim contract:
- Extension tokens carry an explicit `typ` of `openshell-ext+jwt`. They
  share a signing key with sandbox-to-gateway admission tokens and were
  otherwise separated by audience alone, so a verifier that neglects to
  check `aud` could accept a gateway credential. The header is a second,
  independent discriminator.
- Publish OIDC-shaped discovery at `/.well-known/openid-configuration`
  so a service configured with only the gateway URL can learn the exact
  expected issuer and the JWKS location. It is shaped, not compliant:
  `issuer` is the gateway identity, not the serving URL.

Audience agreement:
- `MiddlewareManifest` and `InterceptorManifest` gain `expected_audience`.
  The audience is otherwise configured independently on each side of the
  boundary, where a mismatch surfaces only as an opaque authentication
  failure on every call. OpenShell now compares the two and fails at
  startup. An empty field keeps the check off for existing services.

Compatibility:
- Add `allow_insecure_transport` per registration. Enabling gateway JWT
  signing previously made any plaintext endpoint a hard startup failure,
  including the endpoint form used in our own documentation. The opt-out
  attaches no credential, is refused by the gateway if a supervisor asks
  for one, and warns at every startup.
- Make the transport requirement kind-aware. A middleware endpoint must
  be reachable from every sandbox supervisor, so only interceptors may
  use a gateway-local Unix socket.

Credential lifecycle:
- Replace the process-global slot map with a supervisor-owned
  `ExtensionCredentialStore` shared explicitly across the gateway
  connections the supervisor opens, removing test-order coupling.
- Rotate only when a credential is missing or has passed four fifths of
  its lifetime. Configuration polling ran every ten seconds against
  fifteen-minute credentials, so each poll re-ran gateway effective-policy
  resolution and re-minted the gateway token.
- Bound credential minting per sandbox, since each request resolves the
  caller's effective policy.

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

* docs: record alpha extension authentication in RFC appendices

Restore the RFC 0009 and 0010 bodies to their accepted text and move
every extension-authentication update into appendices instead. An RFC
records a decision at a point in time; superseding detail belongs
alongside it rather than rewritten into it.

RFC 0009's appendix carries the shared contract: claims, authorization,
key distribution, the `allow_insecure_transport` replacement for the
body's `allow_insecure`, and residual risks. RFC 0010's records only
what differs for interceptors and links to it. The existing
protocol-extensions appendix, which parked the phase 2 transport
question, now points forward to what was built.

Also document the audience handshake, the discovery endpoint, the
`typ` requirement, and `jti` replay guidance in the extensibility and
gateway configuration pages, and correct the middleware transport
guidance: middleware endpoints must be reachable from sandbox
supervisors, so Unix sockets are not an option there.

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

* refactor(extension-core): abstract extension server trust

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

* docs(core): update middleware manifest example

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

* fix(extension-auth): preserve unsigned gateway compatibility

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

* fix(extension-auth): reject cross-domain token replay

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>

---------

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
2026-08-14 20:34:10 +00:00

146 lines
5.3 KiB
Protocol Buffer

// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
syntax = "proto3";
package openshell.gateway_interceptor.v1;
import "google/protobuf/struct.proto";
import "openshell.proto";
// GatewayInterceptor lets an external governance service evaluate gateway
// control-plane operations after OpenShell admission and before or after the
// gateway applies the operation.
service GatewayInterceptor {
// Describe returns the interceptor manifest and declared bindings.
rpc Describe(DescribeRequest) returns (InterceptorManifest);
// SnapshotProviderProfiles returns the interceptor's current provider
// profile snapshot when the manifest advertises provider_profiles = true.
rpc SnapshotProviderProfiles(ProviderProfileSnapshotRequest)
returns (ProviderProfileSnapshot);
// Evaluate returns an allow, deny, or mutation decision for one operation
// phase.
rpc Evaluate(InterceptorEvaluation) returns (InterceptorResult);
}
message DescribeRequest {}
message ProviderProfileSnapshotRequest {}
enum GatewayInterceptorPhase {
GATEWAY_INTERCEPTOR_PHASE_UNSPECIFIED = 0;
GATEWAY_INTERCEPTOR_PHASE_MODIFY_OPERATION = 2;
GATEWAY_INTERCEPTOR_PHASE_VALIDATE = 3;
GATEWAY_INTERCEPTOR_PHASE_POST_COMMIT = 4;
}
message InterceptorEvaluation {
// Configured interceptor instance name.
string interceptor_name = 1;
// Manifest binding id selected for this evaluation.
string binding_id = 2;
// Public gRPC service name, e.g. "openshell.v1.OpenShell".
string service = 3;
// Public gRPC method name, e.g. "CreateSandbox".
string method = 4;
// Caller identity summary. Values are intentionally non-secret.
map<string, string> principal = 5;
// Exactly one phase payload is set for each evaluation.
oneof phase {
// Evaluation that may modify the prepared gateway operation.
ModifyOperationEvaluation modify_operation = 6;
// Evaluation that may reject, but not mutate, the prepared operation.
ValidateEvaluation validate = 7;
// Evaluation after the gateway operation has completed successfully.
PostCommitEvaluation post_commit = 8;
}
}
message ModifyOperationEvaluation {
// Gateway-prepared operation the gateway proposes to execute.
google.protobuf.Struct proposed_operation = 1;
}
message ValidateEvaluation {
// Gateway-prepared operation after earlier interceptor modifications.
google.protobuf.Struct proposed_operation = 1;
// Optional read-only gateway state loaded before applying the operation.
google.protobuf.Struct current_state = 2;
}
message PostCommitEvaluation {
// Protobuf JSON-shaped response returned by the committed gateway operation.
google.protobuf.Struct committed_response = 1;
}
message InterceptorResult {
// False denies the operation before side effects for modify_operation and
// validate. Post-commit denial is invalid.
bool allowed = 1;
// Human-readable reason for logs and denied gRPC status messages.
string reason = 2;
// Optional gRPC status code name for denials, e.g. "PERMISSION_DENIED".
string status_code = 3;
// RFC 6902 JSON patches. Only valid during modify_operation.
repeated JsonPatch patches = 4;
// Non-secret annotations included in gateway logs.
map<string, string> log_annotations = 5;
}
message InterceptorManifest {
// Human-readable interceptor name declared by the service.
string name = 1;
// Bindings declared by the interceptor service.
repeated InterceptorBinding bindings = 2;
// Optional default failure policy for bindings without their own policy.
// Supported values are "fail_closed" and "fail_open".
string failure_policy = 3;
// True when this interceptor implements SnapshotProviderProfiles.
bool provider_profiles = 4;
// Exact JWT audience this service verifies on inbound OpenShell calls.
// After authenticated Describe succeeds, the gateway refuses to start unless
// this matches the operator-configured audience. A strict verifier may reject
// an incorrect audience before returning this manifest. Empty skips this
// post-authentication consistency check.
string expected_audience = 5;
}
message ProviderProfileSnapshot {
// Opaque source revision used for cache freshness and sandbox reload checks.
string revision = 1;
// Complete profile snapshot vended by this source.
repeated openshell.v1.ProviderProfile profiles = 2;
}
message InterceptorBinding {
// Stable binding id used for config overrides and audit logs.
string id = 1;
// RPC selector. Selectors are intentionally tied to the public API shape.
InterceptorSelector selector = 2;
// Phases this binding wants to evaluate.
repeated GatewayInterceptorPhase phases = 3;
// Optional binding-specific failure policy.
// Supported values are "fail_closed" and "fail_open".
// Bindings that include post_commit must resolve to "fail_open" because the
// gateway operation has already committed before that phase runs.
string failure_policy = 4;
}
message InterceptorSelector {
// Full selector form: "openshell.v1.OpenShell/CreateSandbox".
string rpc = 1;
// Structured service/method form. If rpc is set, it takes precedence.
string service = 2;
string method = 3;
}
message JsonPatch {
string op = 1;
string path = 2;
google.protobuf.Value value = 3;
string from = 4;
}