envoyproxy / envoyproxy/gateway
SecurityPolicy: JWT Bearer requests rejected by BasicAuth filter when both are configured (no OR semantics)
- Dominant language
- Go
- Stars
- 3k
- Forks
- 864
- Avg merge
- 2d 2h
- Merged PRs (30d)
- 140
Description
## Summary
When a `SecurityPolicy` is configured with both `jwt` and `basicAuth`, a request authenticated with a valid JWT Bearer token is rejected by the BasicAuth filter with:
```
HTTP/2 401
www-authenticate: Basic realm="..."
User authentication failed. Expected 'Basic' authentication scheme.
```
Similarly, a request authenticated with valid Basic credentials (and `jwt.optional` not set) gets rejected by the JWT filter with:
```
HTTP/2 401
www-authenticate: Bearer realm="..."
Jwt is missing
```
Neither direction works — the two filters apply AND logic in practice, even though users expect OR semantics (accept a request if **any one** of the configured auth methods succeeds).
## Environment
- Envoy Gateway version: v1.6.3
- Kubernetes Gateway API version: v1.2.x
## Steps to Reproduce
1. Create a `SecurityPolicy` with both `jwt` and `basicAuth`:
```yaml
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: multi-auth
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: my-route
jwt:
providers:
- name: my-provider
issuer: "https://issuer.example.com"
remoteJWKS:
uri: "https://issuer.example.com/keys"
basicAuth:
users:
name: my-htpasswd-secret
```
2. Send a request with a valid JWT Bearer token → 401 "Expected 'Basic' authentication scheme"
3. Send a request with valid Basic credentials → 401 "Jwt is missing"
## Expected Behavior
A request authenticated via **either** mechanism should be accepted. If JWT validation succeeds, the BasicAuth filter should not run (or should pass through). Likewise, if the request presents valid Basic credentials and no JWT, the JWT filter should not block it. Requests with **no credentials** should still be rejected.
## Actual Behavior
Both filters always run sequentially. Whichever runs second rejects the request if it does not carry the expected credential type for that filter.
Setting `jwt.optional: true` fixes the Basic→JWT direction (JWT filter no longer rejects requests without a Bearer token), but the BasicAuth filter has no equivalent `optional` flag and always rejects requests whose `Authorization` header is not `Basic`.
## Related
- Discussion #2425 describes the same root problem for OIDC+JWT
- Issue #5367 touches on filter ordering
## Workaround
The only currently working workaround is architectural: use **separate SecurityPolicies per route**, routing JWT-authenticated clients to read routes and BasicAuth-authenticated clients to write routes. This is restrictive and may not suit all use cases.
## Proposed Fix
The naive approach of adding `optional: true` to both `jwt` and `basicAuth` is **not sufficient**: if both filters are individually optional, a request with **no credentials at all** would pass through both filters unauthenticated.
What is needed is **"require at least one"** semantics at the `SecurityPolicy` level. For example, a top-level field:
```yaml
requireAnyOne: true # reject if no auth method succeeded; accept if any one did
jwt:
optional: true # pass through if no Bearer token present
providers: [...]
basicAuth:
optional: true # pass through if no Basic credentials present
users:
name: my-secret
```
With this model:
- Request with valid JWT Bearer → JWT filter validates, BasicAuth passes through → **accepted**
- Request with valid Basic credentials → JWT filter passes through, BasicAuth validates → **accepted**
- Request with no credentials → both filters pass through, `requireAnyOne` gate rejects → **401**
- Request with invalid credentials of either type → the relevant filter rejects → **401**
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.