Summary
Add an optional methods field to a service so the credential is attached only to requests using the listed HTTP methods. methods: [GET, HEAD] would broker a privileged token read-only.
Motivation
A service today matches on host + optional port + optional path glob. Any matching request gets the credential, whatever the verb. For most resource-oriented REST APIs, path scoping can't express "read-only": the read and the mutation share a path and differ only by method. On api.github.com, GET /repos/{owner}/{repo} and DELETE /repos/{owner}/{repo} are the same path.
Where the upstream offers a scoped credential (GitHub fine-grained PATs, Stripe restricted keys), that's the better place to enforce read-only. But plenty of internal and legacy REST services issue only an all-or-nothing token. For those, short of trusting the agent's prompt, a method check at the proxy is the only read-only control available.
Proposal
services:
- name: github-readonly
host: api.github.com
methods: [GET, HEAD]
auth:
type: bearer
token: GITHUB_TOKEN
methods omitted = all methods, so every existing config keeps its behavior. Validation rejects an explicit methods: [] rather than treating it as "allow everything."
- Values are uppercase-normalized on ingest and validated against
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS. The request method is compared case-sensitively against that normalized set. An odd-cased method fails closed.
- A request whose method isn't listed is denied, and the credential is not attached as a header or through substitutions (see Design notes).
Separable add-on: method-override stripping. To hold up against upstreams that honor method-override, a service with methods set should also strip X-HTTP-Method-Override / X-Method-Override / X-HTTP-Method and the _method query parameter before forwarding. Stripping modifies the request, which the methods field alone does not, so treat it as a separate yes/no. It's proposed in scope and on by default here, but splits cleanly into a follow-up. It only removes those spoofing tokens; it is not a general header or query matcher. The _method body form would need body inspection and stays out of scope.
Design notes
methods adds no new matcher tier. MatchService still selects the single most-specific host/port/path winner exactly as today. The method check then runs on that winner, parallel to the existing IsEnabled() check in internal/brokercore/credential.go: a winner whose list excludes the method is denied outright, with no fall-through to a broader sibling.
The check has to happen at the Inject boundary, before either credential path runs. Inject resolves both the auth header and any substitutions, and substitutions can write the secret into the path, query, or body (they resolve for passthrough services too). A header-only check would still leak the credential through substitutions on a request it was supposed to deny.
The check also binds the winning service, not the host. A more specific sibling without methods (say path: /repos/*) out-matches a broad methods: [GET] rule and injects on any verb. Making a whole host read-only means putting methods on every overlapping service.
The core plumbing is small: Methods []string on broker.Service plus validation, and passing r.Method into Inject — cheap because both call sites in forwardRequest already hold it. The rest is trailing surfaces: services API validation, web UI form, agent proposal flow, SDKs, docs. Happy to contribute the PR if the shape is agreeable.
Limitations
Method checking constrains plain HTTP request semantics, and only that. A WebSocket handshake is a GET, so a methods: [GET] service still matches it, and a websocket-surface substitution writes the credential into frames that carry no method at all. Upstreams that read _method from a POST body (Rack, Symfony) can still be spoofed, since that would take body inspection. GraphQL-style POST-everything APIs get nothing from this for the same reason.
Open questions
Denial status: every broker-policy denial today is 403 (StatusForbidden in internal/brokercore/brokercore.go), so leaning 403 for consistency. 405 + Allow is more literally correct but would be the ingress's first non-403 broker denial.
Per-method credential splitting (GET gets a read token, POST a write token on the same host) is a different mechanism, a selection-time filter rather than a check on the selected winner, and is not part of this ask.
Summary
Add an optional
methodsfield to a service so the credential is attached only to requests using the listed HTTP methods.methods: [GET, HEAD]would broker a privileged token read-only.Motivation
A service today matches on host + optional port + optional path glob. Any matching request gets the credential, whatever the verb. For most resource-oriented REST APIs, path scoping can't express "read-only": the read and the mutation share a path and differ only by method. On api.github.com,
GET /repos/{owner}/{repo}andDELETE /repos/{owner}/{repo}are the same path.Where the upstream offers a scoped credential (GitHub fine-grained PATs, Stripe restricted keys), that's the better place to enforce read-only. But plenty of internal and legacy REST services issue only an all-or-nothing token. For those, short of trusting the agent's prompt, a method check at the proxy is the only read-only control available.
Proposal
methodsomitted = all methods, so every existing config keeps its behavior. Validation rejects an explicitmethods: []rather than treating it as "allow everything."GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS. The request method is compared case-sensitively against that normalized set. An odd-cased method fails closed.Separable add-on: method-override stripping. To hold up against upstreams that honor method-override, a service with
methodsset should also stripX-HTTP-Method-Override/X-Method-Override/X-HTTP-Methodand the_methodquery parameter before forwarding. Stripping modifies the request, which themethodsfield alone does not, so treat it as a separate yes/no. It's proposed in scope and on by default here, but splits cleanly into a follow-up. It only removes those spoofing tokens; it is not a general header or query matcher. The_methodbody form would need body inspection and stays out of scope.Design notes
methodsadds no new matcher tier.MatchServicestill selects the single most-specific host/port/path winner exactly as today. The method check then runs on that winner, parallel to the existingIsEnabled()check ininternal/brokercore/credential.go: a winner whose list excludes the method is denied outright, with no fall-through to a broader sibling.The check has to happen at the
Injectboundary, before either credential path runs.Injectresolves both the auth header and anysubstitutions, and substitutions can write the secret into the path, query, or body (they resolve forpassthroughservices too). A header-only check would still leak the credential through substitutions on a request it was supposed to deny.The check also binds the winning service, not the host. A more specific sibling without
methods(saypath: /repos/*) out-matches a broadmethods: [GET]rule and injects on any verb. Making a whole host read-only means puttingmethodson every overlapping service.The core plumbing is small:
Methods []stringonbroker.Serviceplus validation, and passingr.MethodintoInject— cheap because both call sites inforwardRequestalready hold it. The rest is trailing surfaces: services API validation, web UI form, agent proposal flow, SDKs, docs. Happy to contribute the PR if the shape is agreeable.Limitations
Method checking constrains plain HTTP request semantics, and only that. A WebSocket handshake is a
GET, so amethods: [GET]service still matches it, and awebsocket-surface substitution writes the credential into frames that carry no method at all. Upstreams that read_methodfrom a POST body (Rack, Symfony) can still be spoofed, since that would take body inspection. GraphQL-style POST-everything APIs get nothing from this for the same reason.Open questions
Denial status: every broker-policy denial today is 403 (
StatusForbiddenininternal/brokercore/brokercore.go), so leaning 403 for consistency. 405 +Allowis more literally correct but would be the ingress's first non-403 broker denial.Per-method credential splitting (GET gets a read token, POST a write token on the same host) is a different mechanism, a selection-time filter rather than a check on the selected winner, and is not part of this ask.