Skip to content

Dashboard backup forms return 403 for tenant users: the console reads CRDs from apiextensions.k8s.io, no tenant role grants it #4053

Description

@IvanHunters

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

  1. Give a user tenant super-admin access (bind cozy:tenant:super-admin in a tenant namespace, or put the user in the <tenant>-super-admin Keycloak group)
  2. Log in to the dashboard as that user
  3. Open Backups and start creating a Plan
  4. 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:32 issues a useK8sGet against apiGroup apiextensions.k8s.io, plural customresourcedefinitions, 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:21 asks for plans.backups.cozystack.io.
  • packages/system/cozystack-basics/templates/clusterroles.yaml defines cozy:tenant:{view,use,admin,super-admin} and never mentions apiextensions.k8s.io. No ClusterRole labeled rbac.cozystack.io/aggregate-to-tenant-* anywhere under packages/ grants customresourcedefinitions.
  • packages/system/backup-controller/templates/tenant-clusterroles.yaml grants the backup CRs themselves (plans, backupjobs, restorejobs, backups, backupclasses) but not the right to read their CRDs. That is why kubectl works for these users and the UI does not.

Affected pages

Everything that calls useCRDSchema, not only Plan:

  • BackupPlanCreatePage
  • BackupJobCreatePage
  • BackupRestoreJobCreatePage
  • BackupCreatePage
  • BackupClassCreatePage
  • BackupClassEditPage
  • BackupResourceEditPage

Possible fixes

  1. Add a ClusterRole labeled rbac.cozystack.io/aggregate-to-tenant-view with get and list on customresourcedefinitions, narrowed by resourceNames to the backups.cozystack.io CRDs. Smallest change and it leaves the console code alone.
  2. Serve the schema through cozystack-api, the way forms for apps.cozystack.io already 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

  1. added
    kind/bugCategorizes issue or PR as related to a bug
    area/platformIssues or PRs related to platform infrastructure (bundle, flux, talos, installer)
    area/dashboardIssues or PRs related to the dashboard / UI
    on Sep 3, 2026
  2. lexfrei commented on Sep 15, 2026

    @lexfrei
    Contributor

    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:view does not reach a cluster-scoped request. Every binding of cozy:tenant:view, use, admin and super-admin in the tree is a RoleBinding scoped to the tenant namespace, in packages/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 list cannot be granted by a rule carrying resourceNames. A list is a collection request and names no object, so resourceNames can never match it. get is also all the console needs here: use-crd-schema.ts does a by-name get.

    The carrier that would work already exists. cozystack-dashboard-readonly in packages/system/dashboard/templates/rbac.yaml is bound to system:authenticated by a ClusterRoleBinding in the same file, and it already holds the cluster-scoped reads the console depends on: storageclasses, applicationdefinitions, backupclasses. Five more resourceNames there 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-api removes the cluster-scoped read instead of trying to grant it.

    Worth knowing while this is open: cozy:tenant:view:base and cozy:tenant:base both list persistentvolumes, also cluster-scoped, in roles bound the same way. Same inert shape, already on main, not this issue's problem.

  3. added 5 commits that reference this issue on Sep 15, 2026
    5f8fd21
    0198c14
    f3207cf
    3a8a06b
    c9aa64c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/dashboardIssues or PRs related to the dashboard / UIarea/platformIssues or PRs related to platform infrastructure (bundle, flux, talos, installer)kind/bugCategorizes issue or PR as related to a bugtriage/needs-triageIndicates an issue needs triage by a maintainer

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions