GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

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

@EhabY

@EhabY EhabY commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Problem

A scoped API key holding api_key:create could mint a key broader than itself:

  • POST /users/{user}/keys/tokens only checked that requested scopes are public, and defaulted to coder:all when none were given.
  • POST /users/{user}/keys (coder login --token, coder open vscode) always minted coder:all with a *:* allow list.
  • GET /applications/auth-redirect minted coder:application_connect with a *:* allow list.
  • /oauth2/authorize ignored the session's own scopes and allow list.

A token scoped to api_key:create user:read minted coder:all on v2.32.0 and on main. Releases from v2.31.7 on are affected, since user:read became public there.

Fix

  • apikey.CapToCaller: a new key must be covered by the caller's scopes (rbac.FirstScopeNotCovered) and allow list (new rbac.FirstAllowListEntryNotCovered). Omitted values inherit the caller's.
  • postToken and postAPIKey refuse anything beyond the caller with 403. Auth-redirect keeps its 404.
  • /oauth2/authorize returns invalid_scope when the grant exceeds the session, or when the session's allow list isn't *:*, since access tokens are minted *:*.
  • A coder:all / *:* session, which is every browser login, behaves as before.

Not in this change

  • Starting a workspace mints a coder:all key for the owner and hands it to the template. Any scoped token that can start the workspace triggers it.
  • createAPIKey could take the caller key, so a new handler can't skip the check.
  • dbauthz InsertAPIKey could enforce the same check.
  • An authorization code could carry the session's allow list instead of refusing restricted sessions.

Verification

Before (v2.32.0) and after, a token scoped to [api_key:create user:read]:

before: POST /users/me/keys/tokens minted [coder:all]
after:  POST /users/me/keys/tokens {"scopes":["user_secret:*"]}: 403 scope "user_secret:*" exceeds the current API key's scopes
after:  POST /users/me/keys/tokens minted [api_key:create user:read]
after:  POST /users/me/keys        minted [api_key:create user:read]

New tests: TestTokenScopeCeiling, TestWorkspaceApplicationAuthScoped, TestOAuth2AuthorizeSessionCeiling, TestFirstAllowListEntryNotCovered. Their refusal and inheritance cases fail without the handler changes.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@github-actions

github-actions Bot commented Oct 1, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

Docs preview

Check 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.

  • docs/admin/integrations/oauth2-provider/scopes.md
  • docs/admin/integrations/oauth2-provider/troubleshooting.md
  • docs/admin/users/sessions-tokens.md
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@github-actions github-actions Bot assigned EhabY Oct 1, 2026
@EhabY
EhabY requested a review from BobbyHo October 1, 2026 16:14
@EhabY
EhabY force-pushed the fix/apikey-scope-ceiling branch from 0233f89 to 9604742 Compare October 1, 2026 17:13
@coderagents

coderagents Bot commented Oct 1, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

Documentation Check

Updates Needed

  • docs/admin/integrations/oauth2-provider/troubleshooting.md (open) - Both session-rejection cases were added to the "invalid_scope" returned to your callback section: scope requests permissions beyond the signed-in session and the signed-in session is restricted to an allow list (marked as standing alone).
  • docs/admin/integrations/oauth2-provider/scopes.md (open) - Added the session ceiling to the conceptual guide: a grant also never exceeds the session authorizing it, and that session must hold every requested scope and an allow list of *:*.

Automated review via Coder Agents

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

BobbyHo
BobbyHo reviewed Oct 1, 2026
View reviewed changes

@BobbyHo BobbyHo left a comment

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for 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.

Uh oh!

There was an error while loading. Please reload this page.

EhabY reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
@BobbyHo

BobbyHo commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

/coder-agents-review

coder-agents-review[bot] reacted with rocket emoji
All reactions
  • 🚀 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@coder-agents-review

coder-agents-review Bot commented Oct 1, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-10-01 21:47 UTC by @BobbyHo

Review history
  • R1 (2026-10-01): 12 reviewers, 2 Nit, 1 P2, 6 P3, 6 P4, COMMENT. Review

deep-review v0.13.0 | Round 1 | a328bee..9604742

Last posted: Round 1, 15 findings (1 P2, 6 P3, 6 P4, 2 Nit), COMMENT. Review

Finding inventory

Finding inventory: PR #30256

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P4 Open coderd/apikey/apikey.go:170 CapToCaller conflates undecidable comparison with refusal R1 Netero Yes
CRF-2 Note Note (body) coderd/oauth2provider/authorize.go:694 Scoped session that passes withinSession still gets 500 on Allow R1 Netero No
CRF-3 P2 Open coderd/apikey/apikey.go:161 New key lifetime not capped at caller expiry; child outlives and survives parent R1 Kurapika P2, Ryosuke P3, Mafuuu P3, Leorio P3, Hisoka P3, Razor P3, Pariston Note Yes
CRF-4 P3 Open docs/admin/users/sessions-tokens.md:128 "Never grants more" is false (lifetime, workspace-start key) R1 Gon, Ryosuke, Mafuuu, Leorio, Hisoka, Razor Yes
CRF-5 P3 Open coderd/oauth2provider/authorize.go:47 errBeyondSession blames scope when session allow list is the cause R1 Gon P3, Leorio P3, Mafuuu P4, Razor P4, Zoro P4 Yes
CRF-6 P4 Open coderd/oauth2provider/authorize.go:191 withinSession duplicates firstScopeBeyondCeiling and drops its logging R1 Ryosuke, Zoro Yes
CRF-7 P3 Open docs/admin/integrations/oauth2-provider/troubleshooting.md:36 New invalid_scope description missing from troubleshooting list R1 Leorio P3, Zoro P4 Yes
CRF-8 P3 Open coderd/apikey/apikey.go:161 Ceiling is opt-in per handler; createAPIKey still defaults to coder:all R1 Ryosuke Yes
CRF-9 P3 Open coderd/apikey/apikey.go:159 CapToCaller name and doc imply clamping; it refuses R1 Gon P3, Zoro P4, Leorio Nit, Kite Nit Yes
CRF-10 P4 Open coderd/apikey.go:236 postAPIKey 403 branch is unreachable R1 Bisky Yes
CRF-11 P3 Open coderd/apikey.go:48 403 message does not name the requesting key's scopes or next step R1 Leorio Yes
CRF-12 P4 Open docs/admin/users/sessions-tokens.md:126 "By default coder:all" now false for scoped callers R1 Leorio, Kite Yes
CRF-13 P4 Open coderd/oauth2provider/authorize.go:32 Block comment says negotiateScope reasons; errBeyondSession is not one R1 Gon Yes
CRF-14 P4 Open coderd/oauth2provider/authorize.go:199 scopeFailureResponse doc says input was logged by negotiateScope R1 Gon Yes
CRF-15 Nit Open coderd/oauth2provider/authorize.go:189 withinSession doc first sentence restates code R1 Gon Yes
CRF-16 P2 Dropped by orchestrator (subsumed by CRF-5 message fix) coderd/oauth2provider/authorize.go:46 errBeyondSession comment restates string R1 Gon No
CRF-17 P2 Dropped by orchestrator (comment unchanged by diff and still accurate) coderd/apikey.go:93 postToken comment restates branch order R1 Gon No
CRF-18 Nit Open coderd/rbac/allowlist_test.go:172 Local l() reimplements slice.New R1 Zoro Yes
CRF-19 Note Note (body) coderd/rbac/allowlist.go:232 FirstAllowListEntryNotCovered matches policy.rego allow-list rule R1 Kurapika, Hisoka, Razor, Ryosuke No
CRF-20 OOS Out of scope (body) coderd/provisionerdserver/provisionerdserver.go:3171 Workspace start mints coder:all key regardless of starter's scope R1 Pariston No
CRF-21 OOS Out of scope (body) coderd/oauth2provider/authorize.go:834 ProcessAuthorize answers code insert authz denial with 500 R1 Netero No
CRF-22 OOS Out of scope (body) coderd/apikey.go:133 Allow-list UUID case not normalized R1 Hisoka No
CRF-23 OOS Out of scope (body) coderd/apikey.go:366 postAPIKey keys (LoginTypePassword) hidden from token list R1 Razor, Kurapika No

Contested and acknowledged

Round log

Round 1

Full 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-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

coder-agents-review[bot]
coder-agents-review Bot reviewed Oct 1, 2026
View reviewed changes

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for 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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread coderd/apikey/apikey.go Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread docs/admin/users/sessions-tokens.md Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread coderd/oauth2provider/authorize.go Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread coderd/oauth2provider/authorize.go Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread coderd/apikey/apikey.go Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread coderd/apikey.go Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread docs/admin/users/sessions-tokens.md Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread coderd/oauth2provider/authorize.go Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread coderd/oauth2provider/authorize.go Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread coderd/rbac/allowlist_test.go Resolved

Uh oh!

There was an error while loading. Please reload this page.

@EhabY
EhabY force-pushed the fix/apikey-scope-ceiling branch from 9604742 to 084d32f Compare October 2, 2026 10:56
@EhabY
EhabY requested a review from BobbyHo October 2, 2026 11:33
BobbyHo
BobbyHo reviewed Oct 5, 2026
View reviewed changes
Comment thread coderd/apikey/apikey.go Resolved

Uh oh!

There was an error while loading. Please reload this page.

EhabY added 3 commits October 6, 2026 13:04
@EhabY
fix: keep new API keys within the caller's scopes and allow list
95fd145
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.
@EhabY
fix: bound new API keys by the caller's lifetime and centralize the c…
c41eede
…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.
@EhabY
fix: name unnamed token keys minted for restricted callers
2b3e116
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.
@EhabY
EhabY force-pushed the fix/apikey-scope-ceiling branch from 084d32f to 2b3e116 Compare October 6, 2026 10:11
@EhabY
EhabY requested a review from BobbyHo October 6, 2026 10:19
BobbyHo
BobbyHo approved these changes Oct 6, 2026
View reviewed changes

@BobbyHo BobbyHo left a comment

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for 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.

Uh oh!

There was an error while loading. Please reload this page.

EhabY reacted with rocket emoji
All reactions
  • 🚀 1 reaction
@EhabY
EhabY merged commit fc01bee into main Oct 7, 2026
32 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@EhabY
EhabY deleted the fix/apikey-scope-ceiling branch October 7, 2026 08:43
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Reviewers

@BobbyHo BobbyHo BobbyHo approved these changes
+1 more reviewer
@coder-agents-review coder-agents-review[bot] coder-agents-review[bot] left review comments
Reviewers whose approvals may not affect merge requirements

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@EhabY EhabY

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@EhabY @BobbyHo
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.