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
Activity
- addedkind/bugCategorizes issue or PR as related to a bugCategorizes issue or PR as related to a bugarea/platformIssues or PRs related to platform infrastructure (bundle, flux, talos, installer)Issues or PRs related to platform infrastructure (bundle, flux, talos, installer)area/dashboardIssues or PRs related to the dashboard / UIIssues or PRs related to the dashboard / UI
on Sep 3, 2026 - addedtriage/needs-triageIndicates an issue needs triage by a maintainerIndicates an issue needs triage by a maintainer
on Sep 3, 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.- added 5 commits that reference this issue
on Sep 15, 2026
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.ioat cluster scope. None of the tenant ClusterRoles grant read access tocustomresourcedefinitions, so the form fails with a 403 for every tenant user, tenant super-admin included. Only a user holding a cluster-wide grant such ascluster-admincan open these forms.Environment
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)To Reproduce
cozy:tenant:super-adminin a tenant namespace, or put the user in the<tenant>-super-adminKeycloak group)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/deleteonbackups.cozystack.io/plansthroughcozy: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:BackupPlanCreatePageBackupJobCreatePageBackupRestoreJobCreatePageBackupCreatePageBackupClassCreatePageBackupClassEditPageBackupResourceEditPagePossible fixes
rbac.cozystack.io/aggregate-to-tenant-viewwithgetandlistoncustomresourcedefinitions, narrowed byresourceNamesto thebackups.cozystack.ioCRDs. Smallest change and it leaves the console code alone.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 toplans,backupjobsandrestorejobs, 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