Repository navigation
Dashboard backup forms return 403 for tenant users: the console reads CRDs from apiextensions.k8s.io, no tenant role grants it #4053
Description
Describe the bug
Every create and edit form under the Backups section of the dashboard builds its form from the CRD object itself, fetched with the logged-in user's token from apiextensions.k8s.io at cluster scope. None of the tenant ClusterRoles grant read access to customresourcedefinitions, so the form fails with a 403 for every tenant user, tenant super-admin included. Only a user holding a cluster-wide grant such as cluster-admin can open these forms.
Environment
- Cozystack version: reproducible on
main; the vendored console that carries this code path is present in every v1.6.x tag (v1.6.0, v1.6.1, v1.6.2) - Provider: not provider-specific, this is an RBAC and UI code path
To Reproduce
- Give a user tenant super-admin access (bind
cozy:tenant:super-adminin a tenant namespace, or put the user in the<tenant>-super-adminKeycloak group) - Log in to the dashboard as that user
- Open Backups and start creating a Plan
- The form never renders and the API returns 403
Expected behaviour
A tenant super-admin, and a tenant admin as well, can open the Plan create form and create the resource. Both already hold create/update/patch/delete on backups.cozystack.io/plans through cozy:backups:admin, so the form should not die on a schema lookup.
Actual behaviour
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "customresourcedefinitions.apiextensions.k8s.io \"plans.backups.cozystack.io\" is forbidden: User \"\" cannot get resource \"customresourcedefinitions\" in API group \"apiextensions.k8s.io\" at the cluster scope",
"reason": "Forbidden",
"details": {
"name": "plans.backups.cozystack.io",
"group": "apiextensions.k8s.io",
"kind": "customresourcedefinitions"
},
"code": 403
}Root cause
packages/system/dashboard/images/console/apps/console/src/lib/use-crd-schema.ts:32issues auseK8sGetagainst apiGroupapiextensions.k8s.io, pluralcustomresourcedefinitions, with the CRD name handed in by the page. The request carries the user's own token, not the console ServiceAccount.packages/system/dashboard/images/console/apps/console/src/routes/BackupPlanCreatePage.tsx:21asks forplans.backups.cozystack.io.packages/system/cozystack-basics/templates/clusterroles.yamldefinescozy:tenant:{view,use,admin,super-admin}and never mentionsapiextensions.k8s.io. No ClusterRole labeledrbac.cozystack.io/aggregate-to-tenant-*anywhere underpackages/grantscustomresourcedefinitions.packages/system/backup-controller/templates/tenant-clusterroles.yamlgrants the backup CRs themselves (plans,backupjobs,restorejobs,backups,backupclasses) but not the right to read their CRDs. That is whykubectlworks for these users and the UI does not.
Affected pages
Everything that calls useCRDSchema, not only Plan:
BackupPlanCreatePageBackupJobCreatePageBackupRestoreJobCreatePageBackupCreatePageBackupClassCreatePageBackupClassEditPageBackupResourceEditPage
Possible fixes
- Add a ClusterRole labeled
rbac.cozystack.io/aggregate-to-tenant-viewwithgetandlistoncustomresourcedefinitions, narrowed byresourceNamesto thebackups.cozystack.ioCRDs. Smallest change and it leaves the console code alone. - Serve the schema through
cozystack-api, the way forms forapps.cozystack.ioalready get theirs, so the console stops reading cluster-scoped resources at all. More work, but it removes the whole class of problem rather than this one instance.
Workaround
Create the resources with kubectl. Tenant admins already have write access to plans, backupjobs and restorejobs, so only the form is blocked.
Logs
See the 403 Status object above. Nothing to redact.
Screenshots
Not applicable, the form area stays empty.
Additional context
One loose end worth flagging: in the report the username in the 403 message is empty (User ""). If that is a redaction by the reporter, the analysis above is complete. If the username really is empty, then identity is not reaching the apiserver at all and no RBAC grant will fix it, which would be a separate bug.
Checklist
- I have checked the documentation
- I have searched for similar issues
- I have included all required information
- I have provided clear steps to reproduce
- I have included relevant logs
Activity
lexfrei commentedon Sep 15, 2026
Possible fix 1 cannot work, and #4236 implemented it faithfully, so the fix landed inert. Two separate reasons, either one enough on its own.
Aggregating into
cozy:tenant:viewdoes not reach a cluster-scoped request. Every binding ofcozy:tenant:view,use,adminandsuper-adminin the tree is a RoleBinding scoped to the tenant namespace, inpackages/apps/tenant/templates/tenant.yaml. A RoleBinding that references a ClusterRole confers that role inside the binding's own namespace; cluster-scoped requests are authorized against ClusterRoleBindings. A repo-wide grep for those four names turns up no ClusterRoleBinding on any tenant persona. So the rule renders, aggregates correctly, and the 403 above stays exactly as it is. The message itself hints at that, ending "at the cluster scope".And
listcannot be granted by a rule carryingresourceNames. A list is a collection request and names no object, soresourceNamescan never match it.getis also all the console needs here:use-crd-schema.tsdoes a by-name get.The carrier that would work already exists.
cozystack-dashboard-readonlyinpackages/system/dashboard/templates/rbac.yamlis bound tosystem:authenticatedby a ClusterRoleBinding in the same file, and it already holds the cluster-scoped reads the console depends on:storageclasses,applicationdefinitions,backupclasses. Five moreresourceNamesthere land in a binding that can authorize a cluster-scoped get. The cost is that it reaches every authenticated user rather than tenant members only, same as the three rules already sitting there.Fix 2 is untouched by any of this and now looks like the better one: serving the schema through
cozystack-apiremoves the cluster-scoped read instead of trying to grant it.Worth knowing while this is open:
cozy:tenant:view:baseandcozy:tenant:baseboth listpersistentvolumes, also cluster-scoped, in roles bound the same way. Same inert shape, already on main, not this issue's problem.fix(dashboard): let dashboard users read backup CRD schemas
fix(dashboard): let dashboard users read backup CRD schemas
fix(dashboard): let dashboard users read backup CRD schemas
fix(dashboard): let dashboard users read backup CRD schemas
fix(dashboard): let dashboard users read backup CRD schemas (#4236)