Skip to content

Commit 7047ab8

Browse files
matt-aitkenTrigger.dev RepoOps
authored andcommitted
docs: state the self-hosted authorization trust boundary
Document the authorization trust boundary for self-hosted installs. Organization membership is the trust boundary on a self-hosted install: everyone in an organization is someone you invited to your own deployment. Separating what an Admin may do from what a Member may do comes from an RBAC plugin that is not part of this distribution, so the permission layer falls back to a permissive ability by design. SECURITY.md now states this explicitly and scopes vulnerability reports against it. Reaching data or actions belonging to an organization you are not a member of, or bypassing authentication, is in scope on every deployment. A member performing a privileged action inside their own organization is out of scope for self-hosted. The webapp contributor guide records the matching rule for code here: enforce tenancy, and leave role separation to the plugin rather than growing the fallback. Mono-RevId: a4f245a2b053007a801e5ef15006148b13735a87
1 parent 1a5ad1e commit 7047ab8

3 files changed

Lines changed: 28 additions & 0 deletions

File tree

SECURITY.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ We take the security of Trigger.dev seriously — for both our Cloud service and
66

77
Unlike Trigger.dev Cloud, the self-hosted setup is optimized for single-tenant use, with code and users you trust. It is not designed to run untrusted code or untrusted payloads.
88

9+
### Scope of a report
10+
11+
A self-hosted deployment is a single trust domain — as above, it is built for code and users you trust. Role separation inside an organization is therefore not a security boundary there: role-based access control comes from a plugin that is not part of the open-source distribution, and without it the permission layer falls back to a permissive ability for session users and personal access tokens. That is deliberate, not an oversight.
12+
13+
- **Out of scope for self-hosted:** a member of an organization performing a privileged action inside that same organization — for example a Member renaming or deleting the organization, or managing other members. Control who you invite, or use Cloud, if you need that separation.
14+
- **In scope from any deployment:** reaching data or actions belonging to an organization the caller is *not* a member of, or bypassing authentication. Organization is a hard boundary on Trigger.dev Cloud, and Cloud runs this same code, so report these even though your own install is single-tenant.
15+
16+
Tell us which deployment you tested against — the same report can be out of scope for self-hosting and in scope for Cloud.
17+
918
## Reporting a vulnerability
1019

1120
**Please do not report security vulnerabilities through public GitHub issues, pull requests, or our Discord.**

apps/webapp/CLAUDE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ The `triggerTask.server.ts` service is the **highest-throughput code path** in t
121121
- Pass the isolation level via options as a string: `{ isolationLevel: "Serializable" }`. Reach for `Serializable` when a read-then-write must be atomic against concurrent transactions (e.g. a count-then-delete invariant); the loser of a race fails and can retry, which is the right trade for rare, correctness-critical paths.
122122
- The helper returns `R | undefined` — guard the result (`if (!result) throw ...`) when callers need a definite value.
123123

124+
## Authorization model: tenancy, not roles
125+
126+
- **This repo enforces tenancy. Roles belong to the RBAC plugin.** Organization is a hard boundary on Cloud, which runs this code, so every query must be scoped such that a non-member cannot reach the row (`members: { some: { userId } }`). A self-hosted install is a single trust domain, but the isolation code is shared, so it is load-bearing here regardless. Anything finer — Admin vs Member, per-resource permissions — is the plugin's job, and the plugin is not part of this repo. The fallback enforces membership itself in a scoped context (`deniedByMembership`); it is the *role* it does not enforce.
127+
- **The permissive fallback is intentional, but it is not universal.** With no plugin loaded a session user or PAT gets a permissive ability (`can: () => true`). Do not "fix" that, and do not add a fallback role floor beside it to compensate. Scoped credentials are the exception: public JWTs and additional API keys get scope-derived abilities from `buildJwtAbility`, and delegated user-actor tokens get cap-derived ones. Those genuinely deny, so never drop an `ability.can(...)` check on a path one of them can reach.
128+
- **Do not add a role check here to close a permission gap.** "A Member can do X inside their own organization" is out of scope for self-hosted — see `SECURITY.md`. The fix belongs in the plugin's policy. Cross-organization access is the opposite case: always a bug, always fix it here.
129+
- **`OrgMemberRole` (`ADMIN` / `MEMBER`) is legacy and capped at its current uses.** It predates the plugin. Do not add a new role, a new permission name, or a role/permission table to this repo. If a change appears to need one, that is a product decision — stop and escalate rather than growing the fallback into a second permission system.
130+
124131
## PAT-authenticated API routes
125132

126133
- **A PAT route must resolve its target org/project scoped to the caller's membership** (`members: { some: { userId } }`, or a helper like `findProjectByRef` / `resolveOrganizationForApiUser`). A PAT is user-scoped and can name any org/project by id/slug, and the OSS RBAC fallback ability is permissive — so `ability.can(...)` alone does NOT reject a non-member on self-hosted. The RBAC `authorization` gate enforces the *role*; the membership-scoped query is the *tenant* floor. Skipping it opens cross-org access on OSS.

docs/self-hosting/security.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ We take the security of Trigger.dev seriously, for both Cloud and self-hosted de
1515
Do not report security vulnerabilities through public GitHub issues, pull requests, or Discord. Use one of the private channels below.
1616
</Warning>
1717

18+
## What is in scope
19+
20+
A self-hosted deployment is a single trust domain: as above, it is built for code and users you trust. Role separation inside an organization is therefore not a security boundary there. Role-based access control comes from a plugin that is not part of the open-source distribution, and without it the permission layer falls back to a permissive ability for session users and personal access tokens. That is deliberate.
21+
22+
**Out of scope for self-hosted:** a member of an organization performing a privileged action inside that same organization, such as renaming or deleting the organization or managing other members. Control who you invite, or use Cloud, if you need that separation.
23+
24+
**In scope from any deployment:** reaching data or actions belonging to an organization the caller is not a member of, or bypassing authentication. Organization is a hard boundary on Trigger.dev Cloud, and Cloud runs this same code, so report these even though your own install is single-tenant.
25+
26+
Tell us which deployment you tested against. The same report can be out of scope for self-hosting and in scope for Cloud.
27+
28+
The [security policy](https://github.com/triggerdotdev/trigger.dev/blob/main/SECURITY.md) is canonical.
29+
1830
## Reporting a vulnerability
1931

2032
<Steps>

0 commit comments

Comments
 (0)