GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — fix: keep new API keys within the caller's scopes and allow list by EhabY · Pull Request #30256 · coder/coder Conversation
Contributor
ProblemA scoped API key holding
A token scoped to Fix
Not in this change
VerificationBefore (v2.32.0) and after, a token scoped to New tests:
Sorry, something went wrong. Uh oh!
There was an error while loading. Please reload this page. All reactions
|
There was an error while loading. Please reload this page.
Docs previewCheck off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
0233f89 to
9604742
Compare
There was an error while loading. Please reload this page.
Documentation CheckUpdates Needed
Automated review via Coder Agents |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Nice fix for the scopes and allow list. One remaining gap is that a newly created key can still outlive the key that created it.
The lifetime is capped by the deployment max, but not by the caller’s expiry. For example, a key with 1 hour remaining and api_key:create could create a token that lasts much longer. It looks like postAPIKey has the same issue.
Since this is part of the “longer-lived” issue from the security report, I’d recommend fixing it in this PR, perhaps by capping ExpiresAt to the caller’s expiry in CapToCaller. If you’d prefer to address it separately, I’m also okay with a follow-up PR/issue.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
/coder-agents-review |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
Chat: Review posted | View chat Review history
deep-review v0.13.0 | Round 1 | Last posted: Round 1, 15 findings (1 P2, 6 P3, 6 P4, 2 Nit), COMMENT. Review Finding inventoryFinding inventory: PR #30256Findings
Contested and acknowledgedRound logRound 1Full panel (round 1). Netero: 1 P4, 1 Note; no P0, panel proceeded. Law not run (308 effective additions). Panel of 12, security-sensitive auth code: ging-go, kurapika, ryosuke, pariston, mafuuu, bisky, gon, leorio, hisoka, razor, kite, wildcard zoro (random from meruem/chopper/zoro/knov). Result: 1 P2, 6 P3, 6 P4, 2 Nit, 2 Note (body), 4 OOS, 2 dropped. Gon comment findings CRF-13/14 downgraded P2 to P4 and CRF-15 to Nit: comment accuracy, no behavior consequence. Lifetime cap (CRF-3) kept in scope: the PR adds the docs claim and CapToCaller is the PR's ceiling primitive; human reviewer BobbyHo raised the same. Reviewed against a328bee..9604742. About deep-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
This PR makes postToken, postAPIKey, auth-redirect, and /oauth2/authorize refuse or inherit instead of minting beyond the caller's scopes and allow list. Findings: 1 P2, 6 P3, 6 P4, 2 Nit.
The P2 is the lifetime gap @BobbyHo raised: a child key is not bounded by the caller's expiry.
Out of scope (needs a ticket or explicit acceptance by a human):
coderd/provisionerdserver/provisionerdserver.go:3171: each workspace start mints a coder:all, *:* key for the owner whatever scope the starting credential had, so a scoped token that can start a workspace and read the agent environment obtains an unscoped key. The PR description lists this without a ticket.coderd/oauth2provider/authorize.go:834: ProcessAuthorize answers a failed InsertOAuth2ProviderAppCode (an authorization denial for a scoped session) with 500 server_error, so the user sees a server fault after clicking Allow.coderd/apikey.go:133: rbac.NewAllowListElement keeps the client's UUID case while ParseAllowListEntry lowercases, so an uppercase workspace:<UUID> entry never matches in policy.rego and now also gets a 403 naming an entry the caller holds.coderd/apikey.go:366: keys minted by POST /users/{user}/keys are LoginTypePassword, so GET /users/{user}/keys/tokens never lists them and the user cannot see or revoke them from the token page.Notes:
+coderd/oauth2provider/authorize.go:694: a scoped session whose scopes cover the grant passes withinSession, then POST returns 500 because no scoped token can hold oauth2_app_code_token:create. No scoped session can complete /oauth2/authorize, and TestOAuth2AuthorizeSessionCeiling has no success case for one.coderd/rbac/allowlist.go:232: FirstAllowListEntryNotCovered admits the same entries as object_is_included_in_scope_allow_list in policy.rego, including *:<id>, so checking scopes and allow list separately bounds the child's authority.docs/admin/integrations/oauth2-provider/troubleshooting.md:36
P3 [CRF-7] This list of invalid_scope descriptions does not include the one this PR adds, and the new one does not open with the requested name as line 36 promises. (Leorio, Zoro)
It is the only rejection a user fixes by signing in with a different credential rather than changing the request or the app's allowlist, and an integrator searching this page for it finds nothing. Add an entry (two, if the allow-list case gets its own description): the signed-in session must hold every requested scope and an allow list of *:*, which every browser login has.
🤖
coderd/oauth2provider/authorize.go:32
P4 [CRF-13] "Rejection reasons from scope negotiation." no longer covers errBeyondSession, which only withinSession returns. (Gon)
Change the header to include the session check, or declare errBeyondSession next to withinSession.
🤖
🤖 This review was automatically generated with Coder Agents.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
9604742 to
084d32f
Compare
There was an error while loading. Please reload this page.
A scoped key holding api_key:create could mint broader keys:
POST /users/{user}/keys/tokens only checked that scopes were public and
defaulted to coder:all, and POST /users/{user}/keys always minted
coder:all. The app auth-redirect and OAuth2 authorize ignored the
caller's scopes and allow list the same way.
New keys must now be covered by the caller's key, and inherit its
scopes and allow list when none are requested. A coder:all session
behaves as before.
…heck createAPIKey now takes the caller's key and applies apikey.InheritWithinCaller (renamed from CapToCaller); login flows use createLoginAPIKey. A caller short of coder:all with *:* also caps the new key's expiry at its own and mints a token, so it cannot slide past it. Coverage failures are a distinct ErrCoverageUndecidable (500), and the 403 detail names the caller's scopes or allow list. The OAuth2 session check reuses firstScopeBeyondCeiling and reports a restricted allow list separately.
Restricted callers get LoginTypeToken keys, which are unique on (user_id, token_name). postAPIKey and the auth redirect set no name, so the second key failed on idx_api_key_name. Generate one like postToken.
084d32f to
2b3e116
Compare
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Thanks for addressing the comments in the PR.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.