The agent command endpoints derive the actor identity and the authorization level from the request body instead of from the authenticated session and the user's real permissions.
Impact
POST /agents/{id}/cmd reads user_id and permission from the JSON body. Because the agent trusts the dashboard's signature and has no user model, an authenticated user could get a dashboard-signed command at an arbitrary authorization level, with a forged audit actor — privilege escalation + audit forgery.
POST /agents/{id}/reboot and POST /agents/{id}/nftables-resolve sign a fixed level without checking the caller actually holds it, and the whole /agents router was gated only by require_auth (no vps:* permission check). register/remove agent had no permission gate either.
Fix
- Derive the actor
user_id from Extension<AuthUser> and the command level from the user's real permissions (vps:read→read, vps:create|edit|delete|vps:*→write, *:*→admin); never from the body.
- Make the
/cmd body a typed {type, payload} so user_id/permission cannot be injected.
- Add a
require_vps_access route floor to the /agents user router, and require >= write on the write operations.
- The signed wire strings the agent consumes are unchanged.
Regression tests assert a low-privilege user is denied /cmd, /reboot, and /nftables-resolve even with permission:"admin" in the body.
The agent command endpoints derive the actor identity and the authorization level from the request body instead of from the authenticated session and the user's real permissions.
Impact
POST /agents/{id}/cmdreadsuser_idandpermissionfrom the JSON body. Because the agent trusts the dashboard's signature and has no user model, an authenticated user could get a dashboard-signed command at an arbitrary authorization level, with a forged audit actor — privilege escalation + audit forgery.POST /agents/{id}/rebootandPOST /agents/{id}/nftables-resolvesign a fixed level without checking the caller actually holds it, and the whole/agentsrouter was gated only byrequire_auth(novps:*permission check).register/removeagent had no permission gate either.Fix
user_idfromExtension<AuthUser>and the command level from the user's real permissions (vps:read→read,vps:create|edit|delete|vps:*→write,*:*→admin); never from the body./cmdbody a typed{type, payload}souser_id/permissioncannot be injected.require_vps_accessroute floor to the/agentsuser router, and require>= writeon the write operations.Regression tests assert a low-privilege user is denied
/cmd,/reboot, and/nftables-resolveeven withpermission:"admin"in the body.