From 8a85a28ca1e2d3c7d4044d6eb6373300f1b8ebab Mon Sep 17 00:00:00 2001 From: Andrew Eacott Date: Thu, 6 Aug 2026 15:55:32 +0200 Subject: [PATCH 01/11] docs(osep): add request-header selector proposal --- ...s-for-credential-binding-disambiguation.md | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md diff --git a/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md b/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md new file mode 100644 index 000000000..38322a2e4 --- /dev/null +++ b/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md @@ -0,0 +1,218 @@ +--- +title: Exact Request-Header Selectors for Credential Binding Disambiguation +authors: + - "@andreweacott" +creation-date: 2026-08-06 +last-updated: 2026-08-06 +status: draft +--- + +# OSEP-0018: Exact Request-Header Selectors for Credential Binding Disambiguation + + +- [Summary](#summary) +- [Motivation](#motivation) + - [Goals](#goals) + - [Non-Goals](#non-goals) +- [Requirements](#requirements) +- [Proposal](#proposal) + - [Notes/Constraints/Caveats](#notesconstraintscaveats) + - [Risks and Mitigations](#risks-and-mitigations) +- [Design Details](#design-details) + - [Data model and API](#data-model-and-api) + - [Matching and selection](#matching-and-selection) + - [Candidate validation](#candidate-validation) + - [Privacy and observability](#privacy-and-observability) + - [SDKs, CLI, and documentation](#sdks-cli-and-documentation) +- [Test Plan](#test-plan) +- [Drawbacks](#drawbacks) +- [Alternatives](#alternatives) +- [Infrastructure Needed](#infrastructure-needed) +- [Upgrade & Migration Strategy](#upgrade--migration-strategy) + + +## Summary + +Add optional exact request-header selectors to Credential Vault bindings. They allow multiple bindings with the same destination scope only when their selectors prove that a request cannot match more than one binding. This is an additive extension to OSEP-0012's fail-closed binding model; it does not introduce a general request-policy language or precedence rule. + +## Motivation + +OSEP-0012 selects credentials by scheme, host, method, and path. If two bindings match the same request at the same host precedence, Credential Proxy fails closed rather than injecting an arbitrary credential. That remains the correct default. + +Some legitimate clients need two credentials for the same destination shape. Two logical environments can share an API host and path while using different API keys, or two package clients can use one private registry endpoint with different authentication forms. Those clients can already send distinct non-secret placeholder values before Credential Proxy injects the real credential. Credential Vault cannot currently use that distinction. + +### Goals + +- Allow a binding to require exact values for one or more existing request headers before it is eligible for credential injection. +- Preserve OSEP-0012's fail-closed behavior when multiple bindings match. +- Keep selector values out of public read APIs, logs, metrics, diagnostics, and error responses. +- Define one wire contract for egress, supported SDKs, CLI output, documentation, and conformance tests. +- Preserve behavior for every existing binding without request-header selectors. + +### Non-Goals + +- Prefix, wildcard, regular-expression, or case-insensitive value matching. +- Selectors over query parameters, request bodies, cookies, or process identity. +- Generic-binding override precedence or any other implicit fallback rule. +- Per-process authorization inside a sandbox. +- Automatic migration of existing bindings. + +## Requirements + +| ID | Requirement | Priority | +| --- | --- | --- | +| R1 | `CredentialMatch` may contain at most eight AND-combined exact request-header predicates. | Must Have | +| R2 | Header names match case-insensitively. Values match case-sensitively after trimming only outer HTTP optional whitespace (SP and HTAB). | Must Have | +| R3 | Each selected request header must occur exactly once; missing or repeated headers do not satisfy a predicate. | Must Have | +| R4 | Selectors permit ordinary end-to-end headers, including `Authorization`, and reject routing, framing, hop-by-hop, and proxy-control headers. | Must Have | +| R5 | Overlapping destination scopes are valid only when selectors prove the bindings cannot both match one request. | Must Have | +| R6 | Public reads return selector header names and `valueConfigured: true`, never selector values. | Must Have | +| R7 | Unsupported selector fields are rejected, never silently removed or broadened. | Must Have | +| R8 | Selectors are non-secret routing hints, not an authorization boundary. | Must Have | + +## Proposal + +Extend `CredentialMatch` with `requestHeaders`: + +```yaml +match: + schemes: [https] + hosts: [registry.example.com] + methods: [GET] + paths: ["/packages/*"] + requestHeaders: + - name: Authorization + value: "Bearer placeholder-client-a" +``` + +Existing scheme, host, method, and path checks run first. A binding is eligible only when every `requestHeaders` predicate matches the original intercepted request. Credential Proxy selects exactly one eligible binding, then applies its existing authentication injection behavior. + +No eligible binding preserves current behavior: inject no credential and let ordinary egress policy decide whether traffic may continue. Multiple eligible bindings retain the current ambiguous-binding denial. + +### Notes/Constraints/Caveats + +- A selector is a routing hint, not authentication. Any sandbox process can construct one, so it cannot restrict a process to a subset of credentials. +- Matching occurs before injection or replacement. A matching placeholder `Authorization` header is subsequently replaced when the selected binding injects `Authorization`. +- Callers retain desired selector values; public read APIs intentionally cannot reconstruct selector-bound bindings. +- This proposal does not widen transparent interception ports or alter egress policy coverage requirements. + +### Risks and Mitigations + +| Risk | Mitigation | +| --- | --- | +| A selector value leaks. | Treat it as non-secret, write-only input and exclude it from public reads, logs, metrics, diagnostics, errors, and examples. | +| Proxy and upstream interpret repeated headers differently. | Require exactly one occurrence and reject routing/framing/proxy-control names. | +| Two bindings select different credentials for one request. | Preserve fail-closed ambiguous matching and allow overlap only when static validation proves predicates disjoint. | +| A mixed egress/SDK deployment broadens a binding. | Add the field additively and require older runtimes to reject it rather than ignore it. | +| Strict validation rejects a useful future policy. | Keep exact conjunctions narrowly scoped; predicate kinds or precedence require a separate proposal. | + +## Design Details + +### Data model and API + +Add `requestHeaders` to the public `CredentialMatch` request schema: + +```yaml +requestHeaders: + type: array + maxItems: 8 + items: + type: object + required: [name, value] + properties: + name: {type: string} + value: {type: string, maxLength: 4096, writeOnly: true} + additionalProperties: false +``` + +After trimming outer HTTP optional whitespace, names and values must be non-empty. A binding must not repeat a header name after case-insensitive normalization. This prevents unsatisfiable predicates. + +Public binding reads return a sanitized match shape: + +```yaml +requestHeaders: + - name: Authorization + valueConfigured: true +``` + +The egress sidecar's private active snapshot retains exact values for matching; public binding metadata never does. + +### Matching and selection + +For each base-matching binding, Credential Proxy evaluates all normalized `requestHeaders` predicates against original request headers: + +1. Header names compare case-insensitively. +2. The selected header must have exactly one occurrence. +3. After trimming only outer SP and HTAB, its value must exactly equal the configured value. +4. Values otherwise receive no case folding, decoding, internal-whitespace normalization, prefix matching, or expression evaluation. + +Existing host precedence is unchanged. At the selected host precedence, zero eligible bindings yields no injection and two or more yields the current ambiguous-binding denial. Selector count is not a precedence dimension. + +### Candidate validation + +Vault creation and mutation validate the complete post-mutation binding set. For bindings with overlapping base scopes, both bindings must declare selectors. They may coexist only if they share a normalized header name whose configured exact values differ, proving no request can satisfy both conjunctions. Otherwise the candidate is rejected. In particular, a selector-bound binding cannot overlap a generic binding, and predicates on different header names do not prove disjointness because a request can carry both headers. + +The rule is deliberately conservative. Failed validation or proxy acknowledgement leaves the previous acknowledged revision active. + +### Privacy and observability + +Do not include selector values in serialized metadata, API errors, structured logs, metrics labels, tracing attributes, diagnostics, test failure output, or documentation examples. Secret-free audit output may include binding name, decision (`matched`, `no_match`, or `ambiguous`), and selected header names. + +### SDKs, CLI, and documentation + +The egress OpenAPI contract is the source of truth. Supported SDKs must preserve `requestHeaders` on create and patch and expose the sanitized read shape. They must not fabricate selector values from a read response. CLI and documentation must identify selector values as write-only non-secret inputs and must not render them in normal inspect/list output. + +## Test Plan + +### Unit and schema tests + +- Accept valid exact conjunctions; reject more than eight entries, blank values, duplicate normalized names, invalid names, and forbidden names. +- Verify case-insensitive names, case-sensitive values, outer-OWS trimming, and no normalization of internal whitespace or value casing. +- Verify missing or duplicate selected headers do not match. +- Verify distinct values for the same header coexist; generic/selector overlap, identical selectors, and different-header selectors are rejected when base scopes overlap. +- Verify reads expose names and `valueConfigured: true`, never values, and that values cannot appear in errors, logs, metrics, or diagnostics. + +### Integration and end-to-end tests + +- Create two bindings for one destination with distinct fake `Authorization` values and verify each request receives only its selected credential. +- Repeat with multiple predicates and verify every predicate is required. +- Verify unmatched traffic continues without injection under normal egress policy. +- Verify an ambiguous candidate revision is rejected without replacing the previous acknowledged revision. +- Run the same request and sanitized-read assertions through all supported SDKs and CLI surfaces that expose Credential Vault. + +## Drawbacks + +- Callers must retain selector values because reads intentionally hide them. +- Exact matching can be stricter than some client ecosystems expect. +- Conservative validation rejects configurations whose disjointness cannot be proven from exact header values alone. +- Operators must understand that this is credential routing, not process-level access control. + +## Alternatives + +### Generic binding overridden by a selector-bound binding + +This supports defaults plus exceptions, but turns the generic binding into a fallback: a missing or unexpected selector could receive the default credential. It also needs a precedence system. This proposal retains fail-closed behavior. + +### Prefix, wildcard, regular-expression, or case-insensitive value matching + +These make mutual exclusivity harder to prove and can hide client mistakes. Exact values meet the known placeholder and authentication-form use cases. + +### Query, body, cookie, or process-identity selectors + +These add parsing, privacy, or authorization semantics beyond credential disambiguation and should be separately proposed. + +### Continue requiring one credential shape per destination + +This preserves current behavior but cannot solve issue #1373's legitimate same-destination credential cases. + +## Infrastructure Needed + +No new service, storage system, or third-party dependency is required. This uses the existing Credential Vault sidecar API, active snapshot, Credential Proxy path, and SDK generation/release process. + +## Upgrade & Migration Strategy + +The change is additive. Existing bindings omit `requestHeaders` and preserve current behavior; no Vault state migration is required. + +Release egress API/runtime support before SDK helpers that create selector-bound bindings. A newer SDK targeting an older sidecar must receive a validation error, never silently omit selectors and create a broader binding. + +Operators adopt selectors by replacing a complete binding through the existing revisioned Vault mutation API. Failed validation or acknowledgement leaves the previous acknowledged revision active. From f6a13b6cf682002486510e61b1bf631eeb983b7a Mon Sep 17 00:00:00 2001 From: Andrew Eacott Date: Thu, 6 Aug 2026 16:01:08 +0200 Subject: [PATCH 02/11] Update 0018-exact-request-header-selectors-for-credential-binding-disambiguation.md --- ...-header-selectors-for-credential-binding-disambiguation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md b/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md index 38322a2e4..6902a3deb 100644 --- a/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md +++ b/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md @@ -61,7 +61,7 @@ Some legitimate clients need two credentials for the same destination shape. Two | ID | Requirement | Priority | | --- | --- | --- | -| R1 | `CredentialMatch` may contain at most eight AND-combined exact request-header predicates. | Must Have | +| R1 | `CredentialMatch` may contain at most four AND-combined exact request-header predicates. | Must Have | | R2 | Header names match case-insensitively. Values match case-sensitively after trimming only outer HTTP optional whitespace (SP and HTAB). | Must Have | | R3 | Each selected request header must occur exactly once; missing or repeated headers do not satisfy a predicate. | Must Have | | R4 | Selectors permit ordinary end-to-end headers, including `Authorization`, and reject routing, framing, hop-by-hop, and proxy-control headers. | Must Have | @@ -166,7 +166,7 @@ The egress OpenAPI contract is the source of truth. Supported SDKs must preserve ### Unit and schema tests -- Accept valid exact conjunctions; reject more than eight entries, blank values, duplicate normalized names, invalid names, and forbidden names. +- Accept valid exact conjunctions; reject more than four entries, blank values, duplicate normalized names, invalid names, and forbidden names. - Verify case-insensitive names, case-sensitive values, outer-OWS trimming, and no normalization of internal whitespace or value casing. - Verify missing or duplicate selected headers do not match. - Verify distinct values for the same header coexist; generic/selector overlap, identical selectors, and different-header selectors are rejected when base scopes overlap. From ed2b0a1210a07558357ebf06fb8242ebe0599b20 Mon Sep 17 00:00:00 2001 From: Andrew Eacott Date: Thu, 6 Aug 2026 16:04:48 +0200 Subject: [PATCH 03/11] docs(osep): clarify selector validation and privacy --- ...der-selectors-for-credential-binding-disambiguation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md b/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md index 6902a3deb..a46ef02fa 100644 --- a/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md +++ b/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md @@ -64,7 +64,7 @@ Some legitimate clients need two credentials for the same destination shape. Two | R1 | `CredentialMatch` may contain at most four AND-combined exact request-header predicates. | Must Have | | R2 | Header names match case-insensitively. Values match case-sensitively after trimming only outer HTTP optional whitespace (SP and HTAB). | Must Have | | R3 | Each selected request header must occur exactly once; missing or repeated headers do not satisfy a predicate. | Must Have | -| R4 | Selectors permit ordinary end-to-end headers, including `Authorization`, and reject routing, framing, hop-by-hop, and proxy-control headers. | Must Have | +| R4 | Selectors permit ordinary end-to-end headers, including `Authorization`, but reject HTTP/2 pseudo-headers and these case-insensitive names: `Host`, `Content-Length`, `Content-Type`, `Transfer-Encoding`, `Connection`, `Upgrade`, `TE`, `Trailer`, `Proxy-Authorization`, `Proxy-Authenticate`, `Forwarded`, `X-Forwarded-For`, `X-Forwarded-Host`, and `X-Forwarded-Proto`. | Must Have | | R5 | Overlapping destination scopes are valid only when selectors prove the bindings cannot both match one request. | Must Have | | R6 | Public reads return selector header names and `valueConfigured: true`, never selector values. | Must Have | | R7 | Unsupported selector fields are rejected, never silently removed or broadened. | Must Have | @@ -100,7 +100,7 @@ No eligible binding preserves current behavior: inject no credential and let ord | Risk | Mitigation | | --- | --- | -| A selector value leaks. | Treat it as non-secret, write-only input and exclude it from public reads, logs, metrics, diagnostics, errors, and examples. | +| A selector value leaks. | Treat it as non-secret, write-only input and exclude configured values from public reads, logs, metrics, diagnostics, and errors. Documentation may use clearly fictional placeholders only. | | Proxy and upstream interpret repeated headers differently. | Require exactly one occurrence and reject routing/framing/proxy-control names. | | Two bindings select different credentials for one request. | Preserve fail-closed ambiguous matching and allow overlap only when static validation proves predicates disjoint. | | A mixed egress/SDK deployment broadens a binding. | Add the field additively and require older runtimes to reject it rather than ignore it. | @@ -125,7 +125,7 @@ requestHeaders: additionalProperties: false ``` -After trimming outer HTTP optional whitespace, names and values must be non-empty. A binding must not repeat a header name after case-insensitive normalization. This prevents unsatisfiable predicates. +`name` must be a non-empty HTTP field-name token as defined by RFC 9110; it is not whitespace-trimmed. After trimming only outer HTTP optional whitespace, `value` must be non-empty. A binding must not repeat a header name after case-insensitive normalization. This prevents unsatisfiable predicates. Public binding reads return a sanitized match shape: @@ -156,7 +156,7 @@ The rule is deliberately conservative. Failed validation or proxy acknowledgemen ### Privacy and observability -Do not include selector values in serialized metadata, API errors, structured logs, metrics labels, tracing attributes, diagnostics, test failure output, or documentation examples. Secret-free audit output may include binding name, decision (`matched`, `no_match`, or `ambiguous`), and selected header names. +Do not include configured selector values in serialized metadata, API errors, structured logs, metrics labels, tracing attributes, diagnostics, or test failure output. Documentation and tests may use clearly fictional placeholder values, never values copied from a real binding. Secret-free audit output may include binding name, decision (`matched`, `no_match`, or `ambiguous`), and selected header names. ### SDKs, CLI, and documentation From d6262a9415cecd5e139f69264f32ebf3d2315909 Mon Sep 17 00:00:00 2001 From: Andrew Eacott Date: Fri, 7 Aug 2026 08:29:04 +0200 Subject: [PATCH 04/11] Update 0018-exact-request-header-selectors-for-credential-binding-disambiguation.md --- ...st-header-selectors-for-credential-binding-disambiguation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md b/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md index a46ef02fa..cf5c87750 100644 --- a/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md +++ b/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md @@ -115,7 +115,7 @@ Add `requestHeaders` to the public `CredentialMatch` request schema: ```yaml requestHeaders: type: array - maxItems: 8 + maxItems: 4 items: type: object required: [name, value] From f06ffc19178e5f93eef542c87c100c79a731879c Mon Sep 17 00:00:00 2001 From: Andrew Eacott Date: Fri, 7 Aug 2026 08:31:03 +0200 Subject: [PATCH 05/11] docs(osep): index request-header selector proposal --- oseps/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/oseps/README.md b/oseps/README.md index 50c98e399..03dd5334c 100644 --- a/oseps/README.md +++ b/oseps/README.md @@ -6,6 +6,7 @@ This is the complete list of OpenSandbox Enhancement Proposals: | OSEP | Title | Status | Last Updated | |:----------------------------------------------------------:|:------------------------------------------:|:-------------:|:------------:| +| [OSEP-0018](0018-exact-request-header-selectors-for-credential-binding-disambiguation.md) | Exact Request-Header Selectors for Credential Binding Disambiguation | draft | 2026-08-06 | | [OSEP-0001](0001-fqdn-based-egress-control.md) | FQDN-based Egress Control | implemented | 2026-01-22 | | [OSEP-0002](0002-kubernetes-sigs-agent-sandbox-support.md) | kubernetes-sigs/agent-sandbox Support | implemented | 2026-01-23 | | [OSEP-0003](0003-volume-and-volumebinding-support.md) | Volume Support | implementing | 2026-02-11 | From 8786ef5c86f63fc2181d2b17c22f4c5d2b446b71 Mon Sep 17 00:00:00 2001 From: Andrew Eacott Date: Fri, 7 Aug 2026 08:33:03 +0200 Subject: [PATCH 06/11] docs(osep): resolve proposal numbering collision --- ...header-selectors-for-credential-binding-disambiguation.md} | 4 ++-- oseps/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename oseps/{0018-exact-request-header-selectors-for-credential-binding-disambiguation.md => 0020-exact-request-header-selectors-for-credential-binding-disambiguation.md} (99%) diff --git a/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md b/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md similarity index 99% rename from oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md rename to oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md index cf5c87750..22252cea4 100644 --- a/oseps/0018-exact-request-header-selectors-for-credential-binding-disambiguation.md +++ b/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md @@ -3,11 +3,11 @@ title: Exact Request-Header Selectors for Credential Binding Disambiguation authors: - "@andreweacott" creation-date: 2026-08-06 -last-updated: 2026-08-06 +last-updated: 2026-08-07 status: draft --- -# OSEP-0018: Exact Request-Header Selectors for Credential Binding Disambiguation +# OSEP-0020: Exact Request-Header Selectors for Credential Binding Disambiguation - [Summary](#summary) diff --git a/oseps/README.md b/oseps/README.md index 03dd5334c..77e39735d 100644 --- a/oseps/README.md +++ b/oseps/README.md @@ -6,7 +6,6 @@ This is the complete list of OpenSandbox Enhancement Proposals: | OSEP | Title | Status | Last Updated | |:----------------------------------------------------------:|:------------------------------------------:|:-------------:|:------------:| -| [OSEP-0018](0018-exact-request-header-selectors-for-credential-binding-disambiguation.md) | Exact Request-Header Selectors for Credential Binding Disambiguation | draft | 2026-08-06 | | [OSEP-0001](0001-fqdn-based-egress-control.md) | FQDN-based Egress Control | implemented | 2026-01-22 | | [OSEP-0002](0002-kubernetes-sigs-agent-sandbox-support.md) | kubernetes-sigs/agent-sandbox Support | implemented | 2026-01-23 | | [OSEP-0003](0003-volume-and-volumebinding-support.md) | Volume Support | implementing | 2026-02-11 | @@ -26,3 +25,4 @@ This is the complete list of OpenSandbox Enhancement Proposals: | [OSEP-0017](0017-resilient-sdk-transport.md) | Resilient SDK Transport | implementing | 2026-07-22 | | [OSEP-0018](0018-execd-as-sandbox-init.md) | execd as Sandbox Init | draft | 2026-07-27 | | [OSEP-0019](0019-node-agent-sandbox-collection.md) | Node Agent for Node-Level Sandbox Collection | implementing | 2026-08-11 | +| [OSEP-0020](0020-exact-request-header-selectors-for-credential-binding-disambiguation.md) | Exact Request-Header Selectors for Credential Binding Disambiguation | draft | 2026-08-07 | From 81e545f44c698fca877e7c8f1cc30ac4e02baeb0 Mon Sep 17 00:00:00 2001 From: Andrew Eacott Date: Fri, 7 Aug 2026 08:40:37 +0200 Subject: [PATCH 07/11] docs(osep): separate selector input and read schemas --- ...s-for-credential-binding-disambiguation.md | 60 ++++++++++++++----- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md b/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md index 22252cea4..95237c148 100644 --- a/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md +++ b/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md @@ -110,24 +110,43 @@ No eligible binding preserves current behavior: inject no credential and let ord ### Data model and API -Add `requestHeaders` to the public `CredentialMatch` request schema: +Define separate input and metadata schemas. `CredentialBinding.match` uses +`CredentialMatch`; `CredentialBindingMetadata.match` uses +`CredentialMatchMetadata`. Both retain the existing base match fields, but +their request-header selector shapes are deliberately distinct: ```yaml -requestHeaders: - type: array - maxItems: 4 - items: - type: object - required: [name, value] - properties: - name: {type: string} - value: {type: string, maxLength: 4096, writeOnly: true} - additionalProperties: false +CredentialMatch: + properties: + requestHeaders: + type: array + maxItems: 4 + items: {$ref: "#/components/schemas/RequestHeaderSelectorInput"} +RequestHeaderSelectorInput: + type: object + required: [name, value] + properties: + name: {type: string} + value: {type: string, maxLength: 4096, writeOnly: true} + additionalProperties: false + +CredentialMatchMetadata: + properties: + requestHeaders: + type: array + items: {$ref: "#/components/schemas/RequestHeaderSelectorMetadata"} +RequestHeaderSelectorMetadata: + type: object + required: [name, valueConfigured] + properties: + name: {type: string} + valueConfigured: {type: boolean, enum: [true], readOnly: true} + additionalProperties: false ``` `name` must be a non-empty HTTP field-name token as defined by RFC 9110; it is not whitespace-trimmed. After trimming only outer HTTP optional whitespace, `value` must be non-empty. A binding must not repeat a header name after case-insensitive normalization. This prevents unsatisfiable predicates. -Public binding reads return a sanitized match shape: +Public binding reads therefore return a sanitized metadata shape: ```yaml requestHeaders: @@ -135,7 +154,10 @@ requestHeaders: valueConfigured: true ``` -The egress sidecar's private active snapshot retains exact values for matching; public binding metadata never does. +The egress sidecar's private active snapshot retains exact values for matching; +public binding metadata never does. The OpenAPI schemas and generated SDK +models must preserve this input/read distinction rather than treating a +metadata response as a `CredentialMatch` write payload. ### Matching and selection @@ -160,7 +182,12 @@ Do not include configured selector values in serialized metadata, API errors, st ### SDKs, CLI, and documentation -The egress OpenAPI contract is the source of truth. Supported SDKs must preserve `requestHeaders` on create and patch and expose the sanitized read shape. They must not fabricate selector values from a read response. CLI and documentation must identify selector values as write-only non-secret inputs and must not render them in normal inspect/list output. +The egress OpenAPI contract is the source of truth. Supported SDKs must expose +the input selector shape on create and patch and the distinct sanitized +metadata shape on get and list. They must not fabricate selector values from a +read response. CLI and documentation must identify selector values as +write-only non-secret inputs and must not render them in normal inspect/list +output. ## Test Plan @@ -170,7 +197,10 @@ The egress OpenAPI contract is the source of truth. Supported SDKs must preserve - Verify case-insensitive names, case-sensitive values, outer-OWS trimming, and no normalization of internal whitespace or value casing. - Verify missing or duplicate selected headers do not match. - Verify distinct values for the same header coexist; generic/selector overlap, identical selectors, and different-header selectors are rejected when base scopes overlap. -- Verify reads expose names and `valueConfigured: true`, never values, and that values cannot appear in errors, logs, metrics, or diagnostics. +- Verify create/patch payloads validate `RequestHeaderSelectorInput`, while + get/list payloads validate `RequestHeaderSelectorMetadata` and expose names + and `valueConfigured: true`, never values. Verify values cannot appear in + errors, logs, metrics, or diagnostics. ### Integration and end-to-end tests From e5fb0d17c29d7f6087145f16f379fa1e03d758da Mon Sep 17 00:00:00 2001 From: Andrew Eacott Date: Fri, 7 Aug 2026 08:43:11 +0200 Subject: [PATCH 08/11] Update 0020-exact-request-header-selectors-for-credential-binding-disambiguation.md --- ...ectors-for-credential-binding-disambiguation.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md b/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md index 95237c148..77f88de9d 100644 --- a/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md +++ b/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md @@ -110,10 +110,11 @@ No eligible binding preserves current behavior: inject no credential and let ord ### Data model and API -Define separate input and metadata schemas. `CredentialBinding.match` uses -`CredentialMatch`; `CredentialBindingMetadata.match` uses -`CredentialMatchMetadata`. Both retain the existing base match fields, but -their request-header selector shapes are deliberately distinct: +Define separate input and metadata schemas: +- `CredentialBinding.match` uses `CredentialMatch` +- `CredentialBindingMetadata.match` uses `CredentialMatchMetadata`. + +Both schema types retain the existing base match fields, but their request-header selector shapes are deliberately seperate: ```yaml CredentialMatch: @@ -154,10 +155,7 @@ requestHeaders: valueConfigured: true ``` -The egress sidecar's private active snapshot retains exact values for matching; -public binding metadata never does. The OpenAPI schemas and generated SDK -models must preserve this input/read distinction rather than treating a -metadata response as a `CredentialMatch` write payload. +The egress sidecar's private active snapshot retains exact values for matching; public binding metadata never does. The OpenAPI schemas and generated SDK models must preserve this input/read distinction rather than treating a metadata response as a `CredentialMatch` write payload. ### Matching and selection From 04e92bcaeb82cd8ed2ecf3cc51b51faffc291e68 Mon Sep 17 00:00:00 2001 From: Andrew Eacott Date: Fri, 7 Aug 2026 08:48:54 +0200 Subject: [PATCH 09/11] docs(osep): preserve host precedence compatibility --- ...s-for-credential-binding-disambiguation.md | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md b/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md index 77f88de9d..f2d8d566a 100644 --- a/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md +++ b/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md @@ -65,7 +65,7 @@ Some legitimate clients need two credentials for the same destination shape. Two | R2 | Header names match case-insensitively. Values match case-sensitively after trimming only outer HTTP optional whitespace (SP and HTAB). | Must Have | | R3 | Each selected request header must occur exactly once; missing or repeated headers do not satisfy a predicate. | Must Have | | R4 | Selectors permit ordinary end-to-end headers, including `Authorization`, but reject HTTP/2 pseudo-headers and these case-insensitive names: `Host`, `Content-Length`, `Content-Type`, `Transfer-Encoding`, `Connection`, `Upgrade`, `TE`, `Trailer`, `Proxy-Authorization`, `Proxy-Authenticate`, `Forwarded`, `X-Forwarded-For`, `X-Forwarded-Host`, and `X-Forwarded-Proto`. | Must Have | -| R5 | Overlapping destination scopes are valid only when selectors prove the bindings cannot both match one request. | Must Have | +| R5 | Bindings whose destination scopes can match a request at the same host precedence are valid only when selectors prove they cannot both match that request. | Must Have | | R6 | Public reads return selector header names and `valueConfigured: true`, never selector values. | Must Have | | R7 | Unsupported selector fields are rejected, never silently removed or broadened. | Must Have | | R8 | Selectors are non-secret routing hints, not an authorization boundary. | Must Have | @@ -170,7 +170,17 @@ Existing host precedence is unchanged. At the selected host precedence, zero eli ### Candidate validation -Vault creation and mutation validate the complete post-mutation binding set. For bindings with overlapping base scopes, both bindings must declare selectors. They may coexist only if they share a normalized header name whose configured exact values differ, proving no request can satisfy both conjunctions. Otherwise the candidate is rejected. In particular, a selector-bound binding cannot overlap a generic binding, and predicates on different header names do not prove disjointness because a request can carry both headers. +Vault creation and mutation validate the complete post-mutation binding set. For +each pair whose base scopes can match a request at the same host precedence, +both bindings must declare selectors. They may coexist only if they share a +normalized header name whose configured exact values differ, proving no request +can satisfy both conjunctions. Otherwise the candidate is rejected. In +particular, a selector-bound binding cannot overlap a generic binding at the +same host precedence, and predicates on different header names do not prove +disjointness because a request can carry both headers. An exact-host binding +and an overlapping wildcard-host binding remain valid without selectors: +existing host precedence selects the exact-host binding rather than treating +the pair as ambiguous. The rule is deliberately conservative. Failed validation or proxy acknowledgement leaves the previous acknowledged revision active. @@ -194,7 +204,11 @@ output. - Accept valid exact conjunctions; reject more than four entries, blank values, duplicate normalized names, invalid names, and forbidden names. - Verify case-insensitive names, case-sensitive values, outer-OWS trimming, and no normalization of internal whitespace or value casing. - Verify missing or duplicate selected headers do not match. -- Verify distinct values for the same header coexist; generic/selector overlap, identical selectors, and different-header selectors are rejected when base scopes overlap. +- Verify distinct values for the same header coexist; generic/selector overlap, + identical selectors, and different-header selectors are rejected when base + scopes can match at the same host precedence. Verify an exact-host binding + and overlapping wildcard-host binding remain valid without selectors and the + exact-host binding is selected. - Verify create/patch payloads validate `RequestHeaderSelectorInput`, while get/list payloads validate `RequestHeaderSelectorMetadata` and expose names and `valueConfigured: true`, never values. Verify values cannot appear in From 6018b292ed9dda66964231507b3ddf7096a9be2d Mon Sep 17 00:00:00 2001 From: Andrew Eacott Date: Fri, 7 Aug 2026 10:36:13 +0200 Subject: [PATCH 10/11] docs(osep): tighten selector contract and compatibility --- ...s-for-credential-binding-disambiguation.md | 128 ++++++++---------- 1 file changed, 57 insertions(+), 71 deletions(-) diff --git a/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md b/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md index f2d8d566a..d323494bd 100644 --- a/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md +++ b/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md @@ -61,14 +61,14 @@ Some legitimate clients need two credentials for the same destination shape. Two | ID | Requirement | Priority | | --- | --- | --- | -| R1 | `CredentialMatch` may contain at most four AND-combined exact request-header predicates. | Must Have | -| R2 | Header names match case-insensitively. Values match case-sensitively after trimming only outer HTTP optional whitespace (SP and HTAB). | Must Have | -| R3 | Each selected request header must occur exactly once; missing or repeated headers do not satisfy a predicate. | Must Have | -| R4 | Selectors permit ordinary end-to-end headers, including `Authorization`, but reject HTTP/2 pseudo-headers and these case-insensitive names: `Host`, `Content-Length`, `Content-Type`, `Transfer-Encoding`, `Connection`, `Upgrade`, `TE`, `Trailer`, `Proxy-Authorization`, `Proxy-Authenticate`, `Forwarded`, `X-Forwarded-For`, `X-Forwarded-Host`, and `X-Forwarded-Proto`. | Must Have | +| R1 | `CredentialMatch` may contain one to four AND-combined exact request-header predicates. | Must Have | +| R2 | Header names match case-insensitively. Configured values and request values match case-sensitively after trimming only outer HTTP optional whitespace (SP and HTAB). | Must Have | +| R3 | Each selected request header must occur exactly once; missing or repeated header fields do not satisfy a predicate. | Must Have | +| R4 | Selectors permit ordinary end-to-end headers, including `Authorization`, but reject HTTP/2 pseudo-headers and these case-insensitive names: `Host`, `Content-Length`, `Content-Type`, `Transfer-Encoding`, `Connection`, `Upgrade`, `TE`, `Trailer`, `Cookie`, `Proxy-Authorization`, `Proxy-Authenticate`, `Forwarded`, `X-Forwarded-For`, `X-Forwarded-Host`, and `X-Forwarded-Proto`. The egress API contract is the maintained source for this fixed denylist; changes are reviewed public-contract changes. | Must Have | | R5 | Bindings whose destination scopes can match a request at the same host precedence are valid only when selectors prove they cannot both match that request. | Must Have | | R6 | Public reads return selector header names and `valueConfigured: true`, never selector values. | Must Have | -| R7 | Unsupported selector fields are rejected, never silently removed or broadened. | Must Have | -| R8 | Selectors are non-secret routing hints, not an authorization boundary. | Must Have | +| R7 | `CredentialMatch` keeps `additionalProperties: false`; runtimes reject unknown properties such as `requestHeaders` rather than silently ignoring them. | Must Have | +| R8 | Selectors are non-secret routing hints, not an authorization boundary; any process in the sandbox can construct a selector that chooses any eligible binding for a destination it can reach. | Must Have | ## Proposal @@ -85,9 +85,9 @@ match: value: "Bearer placeholder-client-a" ``` -Existing scheme, host, method, and path checks run first. A binding is eligible only when every `requestHeaders` predicate matches the original intercepted request. Credential Proxy selects exactly one eligible binding, then applies its existing authentication injection behavior. +Existing scheme, host, method, and path checks determine the highest matching host precedence before selectors are evaluated. A binding at that precedence is eligible only when every `requestHeaders` predicate matches the original intercepted request. Credential Proxy injects a credential only when exactly one binding is eligible. -No eligible binding preserves current behavior: inject no credential and let ordinary egress policy decide whether traffic may continue. Multiple eligible bindings retain the current ambiguous-binding denial. +When no binding is eligible at the highest base host precedence, current behavior is preserved: inject no credential and let ordinary egress policy decide whether traffic may continue. Credential Proxy does not fall back to a lower-precedence binding. Multiple eligible bindings retain the current ambiguous-binding denial. ### Notes/Constraints/Caveats @@ -101,53 +101,47 @@ No eligible binding preserves current behavior: inject no credential and let ord | Risk | Mitigation | | --- | --- | | A selector value leaks. | Treat it as non-secret, write-only input and exclude configured values from public reads, logs, metrics, diagnostics, and errors. Documentation may use clearly fictional placeholders only. | -| Proxy and upstream interpret repeated headers differently. | Require exactly one occurrence and reject routing/framing/proxy-control names. | +| Proxy and upstream interpret repeated headers differently. | Count received header fields before coalescing, require exactly one field, and reject routing, framing, hop-by-hop, proxy-control, forwarding, and explicitly out-of-scope `Cookie` names. | | Two bindings select different credentials for one request. | Preserve fail-closed ambiguous matching and allow overlap only when static validation proves predicates disjoint. | -| A mixed egress/SDK deployment broadens a binding. | Add the field additively and require older runtimes to reject it rather than ignore it. | +| A mixed egress/SDK deployment broadens a binding. | Credential Vault has used strict unknown-field decoding since its initial implementation. Retain that behavior, test it against an older supported runtime, and release runtime support before SDK helpers. | | Strict validation rejects a useful future policy. | Keep exact conjunctions narrowly scoped; predicate kinds or precedence require a separate proposal. | +| A sandbox process spoofs another selector value. | Treat all selector-bound credentials as shared within the sandbox trust boundary. Documentation and UI must not imply per-process authorization; solving that requires a separate proposal. | ## Design Details ### Data model and API -Define separate input and metadata schemas: -- `CredentialBinding.match` uses `CredentialMatch` -- `CredentialBindingMetadata.match` uses `CredentialMatchMetadata`. - -Both schema types retain the existing base match fields, but their request-header selector shapes are deliberately seperate: +Extend the existing `CredentialMatch` schema in place. Both `CredentialBinding.match` and `CredentialBindingMetadata.match` continue to use `CredentialMatch`, preserving the existing public property type in supported SDKs. The selector schema models both directional representations: ```yaml CredentialMatch: properties: requestHeaders: type: array + minItems: 1 maxItems: 4 - items: {$ref: "#/components/schemas/RequestHeaderSelectorInput"} -RequestHeaderSelectorInput: - type: object - required: [name, value] - properties: - name: {type: string} - value: {type: string, maxLength: 4096, writeOnly: true} - additionalProperties: false + items: {$ref: "#/components/schemas/RequestHeaderSelector"} -CredentialMatchMetadata: - properties: - requestHeaders: - type: array - items: {$ref: "#/components/schemas/RequestHeaderSelectorMetadata"} -RequestHeaderSelectorMetadata: +RequestHeaderSelector: type: object - required: [name, valueConfigured] + required: [name] properties: name: {type: string} + value: {type: string, writeOnly: true} valueConfigured: {type: boolean, enum: [true], readOnly: true} + oneOf: + - required: [value] + - required: [valueConfigured] additionalProperties: false ``` -`name` must be a non-empty HTTP field-name token as defined by RFC 9110; it is not whitespace-trimmed. After trimming only outer HTTP optional whitespace, `value` must be non-empty. A binding must not repeat a header name after case-insensitive normalization. This prevents unsatisfiable predicates. +The `oneOf` rejects representations that contain neither directional field or both directional fields. It does not choose the correct operation direction. Create and patch handlers require `name` and `value` and reject `valueConfigured`; get and list serializers require `name` and `valueConfigured` and must never emit `value`. Operation-level schema and conformance tests enforce those directional rules. + +`name` must be a non-empty HTTP field-name token as defined by RFC 9110; it is not whitespace-trimmed. On input, the server trims only outer SP and HTAB from `value`, then rejects an empty result and stores the trimmed value as the canonical value used for matching and ambiguity validation. A binding must not repeat a header name after case-insensitive normalization. These rules prevent unsatisfiable or semantically duplicate predicates. -Public binding reads therefore return a sanitized metadata shape: +Credential Vault creation and `bindings.add` submit complete bindings. `bindings.replace` also replaces a complete binding, including its complete `match`; omitting `requestHeaders` from that replacement means the replacement binding has no selectors. Omitting `bindings` from a mutation, or omitting a binding from its `add`, `replace`, and `delete` sets, leaves that existing binding unchanged. There is no field-level binding merge or per-selector add/remove operation. + +Public binding reads return a sanitized metadata shape: ```yaml requestHeaders: @@ -155,34 +149,28 @@ requestHeaders: valueConfigured: true ``` -The egress sidecar's private active snapshot retains exact values for matching; public binding metadata never does. The OpenAPI schemas and generated SDK models must preserve this input/read distinction rather than treating a metadata response as a `CredentialMatch` write payload. +The egress sidecar's private active snapshot retains canonical values for matching; public binding metadata never does. Adding `requestHeaders` must not change the type of `CredentialBindingMetadata.match` in any supported SDK. ### Matching and selection -For each base-matching binding, Credential Proxy evaluates all normalized `requestHeaders` predicates against original request headers: +Credential Proxy uses this selection order: -1. Header names compare case-insensitively. -2. The selected header must have exactly one occurrence. -3. After trimming only outer SP and HTAB, its value must exactly equal the configured value. -4. Values otherwise receive no case folding, decoding, internal-whitespace normalization, prefix matching, or expression evaluation. +1. Evaluate scheme, host, method, and path for every binding and record each base match's host precedence. +2. Select the highest host precedence among those base matches and discard lower-precedence bindings. +3. Evaluate every normalized `requestHeaders` predicate only for bindings at that precedence: + - Header names compare case-insensitively. + - A selected header must have exactly one received field occurrence before library coalescing. Two field lines do not match; one field line containing a comma remains one occurrence whose entire value is compared. + - After trimming only outer SP and HTAB, the request value must exactly equal the canonical configured value. + - Values otherwise receive no case folding, decoding, internal-whitespace normalization, prefix matching, or expression evaluation. +4. Zero eligible bindings yields no injection. Exactly one is selected. Two or more retain the current ambiguous-binding denial. -Existing host precedence is unchanged. At the selected host precedence, zero eligible bindings yields no injection and two or more yields the current ambiguous-binding denial. Selector count is not a precedence dimension. +Selector count is not a precedence dimension, and a selector miss never falls through to a lower host precedence. ### Candidate validation -Vault creation and mutation validate the complete post-mutation binding set. For -each pair whose base scopes can match a request at the same host precedence, -both bindings must declare selectors. They may coexist only if they share a -normalized header name whose configured exact values differ, proving no request -can satisfy both conjunctions. Otherwise the candidate is rejected. In -particular, a selector-bound binding cannot overlap a generic binding at the -same host precedence, and predicates on different header names do not prove -disjointness because a request can carry both headers. An exact-host binding -and an overlapping wildcard-host binding remain valid without selectors: -existing host precedence selects the exact-host binding rather than treating -the pair as ambiguous. +Vault creation and mutation validate the complete post-mutation binding set. For any pair of bindings whose base scopes can match the same request at the same host precedence, both must declare selectors, and they may coexist only if they share a normalized header name whose canonical configured values differ, proving no request can satisfy both conjunctions. Any other such pair is rejected. Two corollaries follow: a selector-bound binding can never overlap a generic binding at the same host precedence, and predicates on different header names never prove disjointness because a single request can carry both headers. Exact-host and overlapping wildcard-host bindings remain valid without selectors because host precedence already picks the exact-host binding rather than treating the pair as ambiguous. -The rule is deliberately conservative. Failed validation or proxy acknowledgement leaves the previous acknowledged revision active. +The rule is deliberately conservative. Failed validation or acknowledgement leaves the previous acknowledged revision active. ### Privacy and observability @@ -190,29 +178,23 @@ Do not include configured selector values in serialized metadata, API errors, st ### SDKs, CLI, and documentation -The egress OpenAPI contract is the source of truth. Supported SDKs must expose -the input selector shape on create and patch and the distinct sanitized -metadata shape on get and list. They must not fabricate selector values from a -read response. CLI and documentation must identify selector values as -write-only non-secret inputs and must not render them in normal inspect/list -output. +The egress OpenAPI contract is the source of truth. Supported SDKs add the optional `requestHeaders` property to the existing `CredentialMatch` model and preserve `CredentialBindingMetadata.match` as `CredentialMatch`. SDK and CLI write paths accept `name` and `value`; get, list, and inspect surfaces expose only `name` and `valueConfigured`. They must not fabricate selector values from a read response. CLI and documentation must identify selector values as write-only non-secret inputs and must not render them in normal inspect/list output. ## Test Plan ### Unit and schema tests -- Accept valid exact conjunctions; reject more than four entries, blank values, duplicate normalized names, invalid names, and forbidden names. -- Verify case-insensitive names, case-sensitive values, outer-OWS trimming, and no normalization of internal whitespace or value casing. -- Verify missing or duplicate selected headers do not match. -- Verify distinct values for the same header coexist; generic/selector overlap, - identical selectors, and different-header selectors are rejected when base - scopes can match at the same host precedence. Verify an exact-host binding - and overlapping wildcard-host binding remain valid without selectors and the - exact-host binding is selected. -- Verify create/patch payloads validate `RequestHeaderSelectorInput`, while - get/list payloads validate `RequestHeaderSelectorMetadata` and expose names - and `valueConfigured: true`, never values. Verify values cannot appear in - errors, logs, metrics, or diagnostics. +- Accept valid exact conjunctions; reject empty arrays, more than four entries, blank values, duplicate normalized names, invalid names, `Cookie`, and other forbidden names. +- Verify case-insensitive names, case-sensitive values, canonical outer-OWS trimming on configured and request values, and no normalization of internal whitespace or value casing. Verify values that differ only in outer SP/HTAB do not prove two bindings disjoint. +- Verify a selected header with two received field lines does not match, while one comma-containing field is one occurrence whose entire value is compared. +- Verify distinct values for the same header coexist; generic/selector overlap, identical selectors, and different-header selectors are rejected when base scopes can match at the same host precedence. +- Verify an exact-host binding and overlapping wildcard-host binding remain valid without selectors and the exact-host binding is selected. +- Verify an exact-host selector miss yields no injection and does not fall through to an overlapping generic wildcard-host binding. +- Verify selector representations containing neither `value` nor `valueConfigured`, or both, fail schema validation. Verify create/patch rejects `valueConfigured`, while get/list never exposes `value` and requires `valueConfigured: true`. +- Verify configured selector values cannot appear in errors, logs, metrics, diagnostics, or test failure output. +- Across supported SDKs, compile existing selector-free constructors/builders and verify their serialized `CredentialMatch` representation is unchanged. Preserve `CredentialMatch` as the type of `CredentialBindingMetadata.match`. +- Verify `bindings.replace` replaces the complete binding: omitting `requestHeaders` removes the previous selectors. Verify a mutation that does not name an existing binding leaves it unchanged. +- Verify a newer payload containing `requestHeaders` is rejected, not silently accepted without effect, by an older supported Credential Vault runtime. ### Integration and end-to-end tests @@ -247,14 +229,18 @@ These add parsing, privacy, or authorization semantics beyond credential disambi This preserves current behavior but cannot solve issue #1373's legitimate same-destination credential cases. +### Fork `CredentialMatch` into distinct write/read types + +Mirroring `CredentialAuth`/`CredentialAuthMetadata` would give SDKs compile-time enforcement of the write/read shape split. It is rejected because `CredentialBindingMetadata.match` has always been typed as `CredentialMatch`, unlike `auth`, which was split from the start. Forking it now would break existing selector-free consumers across supported SDKs and belongs only in an explicit, versioned breaking migration. + ## Infrastructure Needed -No new service, storage system, or third-party dependency is required. This uses the existing Credential Vault sidecar API, active snapshot, Credential Proxy path, and SDK generation/release process. +No new service, storage system, capability protocol, or third-party dependency is required. This uses the existing Credential Vault sidecar API, active snapshot, Credential Proxy path, strict JSON decoding, and SDK generation/release process. ## Upgrade & Migration Strategy The change is additive. Existing bindings omit `requestHeaders` and preserve current behavior; no Vault state migration is required. -Release egress API/runtime support before SDK helpers that create selector-bound bindings. A newer SDK targeting an older sidecar must receive a validation error, never silently omit selectors and create a broader binding. +Release egress API/runtime support before SDK helpers that create selector-bound bindings. Credential Vault-capable runtimes have used strict unknown-field decoding since the feature's initial implementation, so an older runtime rejects `requestHeaders` instead of silently creating a broader binding. Preserve that behavior with a compatibility test; no capability-negotiation mechanism is introduced. Operators adopt selectors by replacing a complete binding through the existing revisioned Vault mutation API. Failed validation or acknowledgement leaves the previous acknowledged revision active. From d058e90f6a3fedddbc8da85bbd59cbd2d7968159 Mon Sep 17 00:00:00 2001 From: Andrew Eacott Date: Wed, 12 Aug 2026 17:25:24 +0100 Subject: [PATCH 11/11] docs(osep): address selector review feedback --- ...s-for-credential-binding-disambiguation.md | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md b/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md index d323494bd..913b86f8d 100644 --- a/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md +++ b/oseps/0020-exact-request-header-selectors-for-credential-binding-disambiguation.md @@ -64,9 +64,9 @@ Some legitimate clients need two credentials for the same destination shape. Two | R1 | `CredentialMatch` may contain one to four AND-combined exact request-header predicates. | Must Have | | R2 | Header names match case-insensitively. Configured values and request values match case-sensitively after trimming only outer HTTP optional whitespace (SP and HTAB). | Must Have | | R3 | Each selected request header must occur exactly once; missing or repeated header fields do not satisfy a predicate. | Must Have | -| R4 | Selectors permit ordinary end-to-end headers, including `Authorization`, but reject HTTP/2 pseudo-headers and these case-insensitive names: `Host`, `Content-Length`, `Content-Type`, `Transfer-Encoding`, `Connection`, `Upgrade`, `TE`, `Trailer`, `Cookie`, `Proxy-Authorization`, `Proxy-Authenticate`, `Forwarded`, `X-Forwarded-For`, `X-Forwarded-Host`, and `X-Forwarded-Proto`. The egress API contract is the maintained source for this fixed denylist; changes are reviewed public-contract changes. | Must Have | +| R4 | Selectors permit ordinary end-to-end headers, including `Authorization` and `Content-Type`, but reject HTTP/2 pseudo-headers and these case-insensitive names: `Host`, `Content-Length`, `Transfer-Encoding`, `Connection`, `Upgrade`, `TE`, `Trailer`, `Cookie`, `Proxy-Authorization`, `Proxy-Authenticate`, `Forwarded`, `X-Forwarded-For`, `X-Forwarded-Host`, and `X-Forwarded-Proto`. `Cookie` is excluded because clients and intermediaries can combine or reorder cookie fields, making exact raw-header matching unreliable. The egress API contract is the maintained source for this fixed denylist; changes are reviewed public-contract changes. | Must Have | | R5 | Bindings whose destination scopes can match a request at the same host precedence are valid only when selectors prove they cannot both match that request. | Must Have | -| R6 | Public reads return selector header names and `valueConfigured: true`, never selector values. | Must Have | +| R6 | Public reads return selector header names, never selector values. | Must Have | | R7 | `CredentialMatch` keeps `additionalProperties: false`; runtimes reject unknown properties such as `requestHeaders` rather than silently ignoring them. | Must Have | | R8 | Selectors are non-secret routing hints, not an authorization boundary; any process in the sandbox can construct a selector that chooses any eligible binding for a destination it can reach. | Must Have | @@ -87,7 +87,7 @@ match: Existing scheme, host, method, and path checks determine the highest matching host precedence before selectors are evaluated. A binding at that precedence is eligible only when every `requestHeaders` predicate matches the original intercepted request. Credential Proxy injects a credential only when exactly one binding is eligible. -When no binding is eligible at the highest base host precedence, current behavior is preserved: inject no credential and let ordinary egress policy decide whether traffic may continue. Credential Proxy does not fall back to a lower-precedence binding. Multiple eligible bindings retain the current ambiguous-binding denial. +When no binding is selector-eligible at the highest base host precedence, Credential Proxy denies the request with a `403` selector-mismatch response. It does not fall back to a lower-precedence binding or allow unauthenticated traffic to continue. Multiple eligible bindings retain the current ambiguous-binding denial. ### Notes/Constraints/Caveats @@ -111,31 +111,48 @@ When no binding is eligible at the highest base host precedence, current behavio ### Data model and API -Extend the existing `CredentialMatch` schema in place. Both `CredentialBinding.match` and `CredentialBindingMetadata.match` continue to use `CredentialMatch`, preserving the existing public property type in supported SDKs. The selector schema models both directional representations: +Extend the existing read-facing `CredentialMatch` schema in place. `CredentialBindingMetadata.match` continues to use `CredentialMatch`, preserving the existing public property type in supported SDKs. Create and patch request models use `CredentialMatchInput`, so OpenAPI validates the write shape without changing the stable read-facing type: ```yaml CredentialMatch: + type: object + # Existing schemes, hosts, methods, and paths remain unchanged. properties: requestHeaders: type: array minItems: 1 maxItems: 4 items: {$ref: "#/components/schemas/RequestHeaderSelector"} + additionalProperties: false RequestHeaderSelector: type: object required: [name] + properties: + name: {type: string} + additionalProperties: false + +CredentialMatchInput: + type: object + # Same schemes, hosts, methods, and paths as CredentialMatch. + properties: + requestHeaders: + type: array + minItems: 1 + maxItems: 4 + items: {$ref: "#/components/schemas/RequestHeaderSelectorInput"} + additionalProperties: false + +RequestHeaderSelectorInput: + type: object + required: [name, value] properties: name: {type: string} value: {type: string, writeOnly: true} - valueConfigured: {type: boolean, enum: [true], readOnly: true} - oneOf: - - required: [value] - - required: [valueConfigured] additionalProperties: false ``` -The `oneOf` rejects representations that contain neither directional field or both directional fields. It does not choose the correct operation direction. Create and patch handlers require `name` and `value` and reject `valueConfigured`; get and list serializers require `name` and `valueConfigured` and must never emit `value`. Operation-level schema and conformance tests enforce those directional rules. +`CredentialBindingInput.match` uses `CredentialMatchInput`; get and list serializers use `CredentialMatch` and emit only `name` for each selector. They must never emit `value`. Operation-level schema and conformance tests enforce those directional rules. `name` must be a non-empty HTTP field-name token as defined by RFC 9110; it is not whitespace-trimmed. On input, the server trims only outer SP and HTAB from `value`, then rejects an empty result and stores the trimmed value as the canonical value used for matching and ambiguity validation. A binding must not repeat a header name after case-insensitive normalization. These rules prevent unsatisfiable or semantically duplicate predicates. @@ -146,7 +163,6 @@ Public binding reads return a sanitized metadata shape: ```yaml requestHeaders: - name: Authorization - valueConfigured: true ``` The egress sidecar's private active snapshot retains canonical values for matching; public binding metadata never does. Adding `requestHeaders` must not change the type of `CredentialBindingMetadata.match` in any supported SDK. @@ -162,10 +178,12 @@ Credential Proxy uses this selection order: - A selected header must have exactly one received field occurrence before library coalescing. Two field lines do not match; one field line containing a comma remains one occurrence whose entire value is compared. - After trimming only outer SP and HTAB, the request value must exactly equal the canonical configured value. - Values otherwise receive no case folding, decoding, internal-whitespace normalization, prefix matching, or expression evaluation. -4. Zero eligible bindings yields no injection. Exactly one is selected. Two or more retain the current ambiguous-binding denial. +4. Zero eligible bindings yields a `403` selector-mismatch denial. Exactly one is selected. Two or more retain the current ambiguous-binding denial. Selector count is not a precedence dimension, and a selector miss never falls through to a lower host precedence. +The existing encoded-slash safety check compares selector-eligible bindings, not base matches alone. For both the raw path and its single-decoded form, Credential Proxy applies the same scheme, host, method, path, host-precedence, and request-header-selector rules. It rejects the request only when decoding changes the resulting eligible binding set or selection outcome. A binding introduced only by decoding that fails its selectors does not itself make the path ambiguous. + ### Candidate validation Vault creation and mutation validate the complete post-mutation binding set. For any pair of bindings whose base scopes can match the same request at the same host precedence, both must declare selectors, and they may coexist only if they share a normalized header name whose canonical configured values differ, proving no request can satisfy both conjunctions. Any other such pair is rejected. Two corollaries follow: a selector-bound binding can never overlap a generic binding at the same host precedence, and predicates on different header names never prove disjointness because a single request can carry both headers. Exact-host and overlapping wildcard-host bindings remain valid without selectors because host precedence already picks the exact-host binding rather than treating the pair as ambiguous. @@ -178,7 +196,7 @@ Do not include configured selector values in serialized metadata, API errors, st ### SDKs, CLI, and documentation -The egress OpenAPI contract is the source of truth. Supported SDKs add the optional `requestHeaders` property to the existing `CredentialMatch` model and preserve `CredentialBindingMetadata.match` as `CredentialMatch`. SDK and CLI write paths accept `name` and `value`; get, list, and inspect surfaces expose only `name` and `valueConfigured`. They must not fabricate selector values from a read response. CLI and documentation must identify selector values as write-only non-secret inputs and must not render them in normal inspect/list output. +The egress OpenAPI contract is the source of truth. Supported SDKs add the optional `requestHeaders` property to the existing `CredentialMatch` model and preserve `CredentialBindingMetadata.match` as `CredentialMatch`. SDK and CLI write paths accept `name` and `value`; get, list, and inspect surfaces expose only `name`. They must not fabricate selector values from a read response. CLI and documentation must identify selector values as write-only non-secret inputs and must not render them in normal inspect/list output. ## Test Plan @@ -189,8 +207,9 @@ The egress OpenAPI contract is the source of truth. Supported SDKs add the optio - Verify a selected header with two received field lines does not match, while one comma-containing field is one occurrence whose entire value is compared. - Verify distinct values for the same header coexist; generic/selector overlap, identical selectors, and different-header selectors are rejected when base scopes can match at the same host precedence. - Verify an exact-host binding and overlapping wildcard-host binding remain valid without selectors and the exact-host binding is selected. -- Verify an exact-host selector miss yields no injection and does not fall through to an overlapping generic wildcard-host binding. -- Verify selector representations containing neither `value` nor `valueConfigured`, or both, fail schema validation. Verify create/patch rejects `valueConfigured`, while get/list never exposes `value` and requires `valueConfigured: true`. +- Verify an exact-host selector miss returns a `403` selector-mismatch denial and does not fall through to an overlapping generic wildcard-host binding. +- Verify create/patch selector payloads require `name` and `value`, while get/list responses expose only `name` and never expose `value`. +- Verify encoded-slash safety remains fail-closed when decoding changes the selected binding or makes the request ambiguous. Verify it does not reject a request merely because decoding adds a base-matching binding whose request-header selectors are not satisfied. - Verify configured selector values cannot appear in errors, logs, metrics, diagnostics, or test failure output. - Across supported SDKs, compile existing selector-free constructors/builders and verify their serialized `CredentialMatch` representation is unchanged. Preserve `CredentialMatch` as the type of `CredentialBindingMetadata.match`. - Verify `bindings.replace` replaces the complete binding: omitting `requestHeaders` removes the previous selectors. Verify a mutation that does not name an existing binding leaves it unchanged. @@ -200,7 +219,7 @@ The egress OpenAPI contract is the source of truth. Supported SDKs add the optio - Create two bindings for one destination with distinct fake `Authorization` values and verify each request receives only its selected credential. - Repeat with multiple predicates and verify every predicate is required. -- Verify unmatched traffic continues without injection under normal egress policy. +- Verify a selector mismatch is denied without injection or lower-precedence fallback. - Verify an ambiguous candidate revision is rejected without replacing the previous acknowledged revision. - Run the same request and sanitized-read assertions through all supported SDKs and CLI surfaces that expose Credential Vault. @@ -229,9 +248,9 @@ These add parsing, privacy, or authorization semantics beyond credential disambi This preserves current behavior but cannot solve issue #1373's legitimate same-destination credential cases. -### Fork `CredentialMatch` into distinct write/read types +### Fork the public SDK-facing `CredentialMatch` type -Mirroring `CredentialAuth`/`CredentialAuthMetadata` would give SDKs compile-time enforcement of the write/read shape split. It is rejected because `CredentialBindingMetadata.match` has always been typed as `CredentialMatch`, unlike `auth`, which was split from the start. Forking it now would break existing selector-free consumers across supported SDKs and belongs only in an explicit, versioned breaking migration. +Mirroring `CredentialAuth`/`CredentialAuthMetadata` would give SDKs compile-time enforcement of the write/read shape split. It is rejected because `CredentialBindingMetadata.match` has always been typed as `CredentialMatch`, unlike `auth`, which was split from the start. Forking that public read-facing type now would break existing selector-free consumers across supported SDKs and belongs only in an explicit, versioned breaking migration. `CredentialMatchInput` is an OpenAPI request shape, not a replacement for that public read-facing type. ## Infrastructure Needed