Repository navigation
feat(gateway): verify Cloudflare Access edge tokens - #4866
ericcurtin wants to merge 4 commits into
Conversation
Signed-off-by: Eric Curtin <eric.curtin@docker.com>
|
@mrunalp @johntmyers PTAL, and |
mrunalp
left a comment
There was a problem hiding this comment.
Thanks for this. The structure looks right: JwksCache is reused through TokenKind, the gateway refuses to start with edge auth alongside OIDC or allow_unauthenticated_users, and a present-but-invalid token stops the chain. I also checked the WebSocket tunnel path. The SDK interceptor puts cf-access-jwt-assertion on every inner gRPC call, and /_ws_tunnel hands those calls back to MultiplexService, so they go through the same auth chain.
A few things to settle before approving:
1. OIDC users with an empty sub are now rejected.
Before this PR, an OIDC token with "sub": "" was accepted: sub only had to be present. With this PR, the empty-subject check sits in the shared validate_token, so OIDC deployments also start returning Unauthenticated: empty subject. It's sensible hardening, but the PR description doesn't mention it and it's outside the issue's scope. Could you either note it in the release notes or limit the check to TokenKind::CloudflareAccess? (crates/openshell-server/src/auth/oidc.rs:1003-1011)
2. The default setup still makes every caller an admin.
Before this PR, every caller behind Access was one shared admin principal. With this PR and no roles configured, each caller gets their own identity, but every one is still a platform admin and workspace membership isn't checked. That includes the config #4813 suggests, which sets only roles_claim. The issue's first acceptance criterion says the subject "is checked against workspace membership", which only happens once both admin_role and user_role are set. This matches how OIDC behaves without roles, and there's a startup warning (crates/openshell-server/src/cli.rs:761) plus docs. We should still decide whether this mode is an acceptable default or should need an explicit opt-in.
3. Automation using Access service tokens has no path in edge mode.
Before this PR, automation using Cloudflare Access service tokens got through as the local-dev principal. With edge_auth on, those tokens are rejected for having an empty sub. OIDC and allow_unauthenticated_users can't be combined with edge auth, and the documented Helm setup disables TLS, which rules out mTLS. Operators who switch would lose that automation entirely. It's documented and opt-in, so it isn't a regression. Should service tokens be mapped via common_name here, or in a follow-up?
4. The main security promise has no direct test.
A request with no edge token is rejected rather than treated as local-dev only because of two things together: validate_config forbids allow_unauthenticated_users, and the chain is non-empty. No router-level test builds a ServerState with edge_auth and sends a request without a token. rejected_edge_credential_emits_classified_authentication_failure uses a mock rejection, and the authenticator test stops at NotApplicable. Since this is the issue's second acceptance criterion, a test of that whole path would protect it from future refactors. The E2E box is also unchecked.
5. Nit: only the first Cookie header is read.
HTTP/2 lets clients split cookies across several cookie fields, and edge_credential reads only the first (crates/openshell-server/src/auth/edge.rs:37). The CLI and SDK send the header, so they're unaffected; a split-cookie client would get NotApplicable and then Unauthenticated.
Out of scope, for context: browser-facing sandbox service routing (service_routing.rs) doesn't check the gateway user at all, and this PR doesn't change that. The docs correctly say gRPC callers that bypass the proxy are rejected. The issue's "every request" wording is broader than what ships.
Signed-off-by: Eric Curtin <eric.curtin@docker.com>
Reject empty subjects for edge tokens only, read split Cookie headers, and test the router rejects tokenless requests. Signed-off-by: Eric Curtin <eric.curtin@docker.com>
|
@mrunalp thanks. Merged main and:
PTAL, and |
Signed-off-by: Eric Curtin <eric.curtin@docker.com>
|
Merged main and fixed the conflicts. @mrunalp PTAL, and |
Summary
Verify the Cloudflare Access token on every gRPC request, so bypassing the proxy no longer grants admin.
Related Issue
Closes #4813
Changes
[openshell.gateway.edge_auth]and anEdgeAuthenticator.JwksCachefor key fetch, rotation, and validation.edge_authwith OIDC orallow_unauthenticated_users.Without
admin_roleanduser_role, every admitted caller is a platform admin (as with mTLS). Startup warns.Release Notes
Gateways behind Cloudflare Access can verify the Access token with
[openshell.gateway.edge_auth]instead ofallow_unauthenticated_users.Testing
Ran
cargo fmt,cargo clippy -D warnings,cargo testforopenshell-serverandopenshell-core, markdownlint, andfern check.Checklist