Skip to content

RBAC: ClusterRole grants cluster-wide write access to Services/ConfigMaps/Deployments/PDBs across every namespace #7

Description

@jordigilh

Summary

deploy/rbac.yaml grants the operator's ClusterRole cluster-scoped
create/update/patch/delete on Services, ConfigMaps, Deployments,
and PodDisruptionBudgets across every namespace in the cluster (bound
via a matching ClusterRoleBinding, not a per-namespace Role). This is a
wide blast radius for a controller whose actual job is to manage resources it
owns in namespaces where a Gateway/HTTPRoute happens to live, and it
isn't documented anywhere as an accepted trade-off.

To be clear up front: this isn't as bad as it could be. Secrets and
Endpoints are already correctly scoped to read-only (get/list/watch)
with an explanatory comment ("Secrets (TLS material) and Endpoints are only
ever read."), and the PodDisruptionBudget write access has its own
rationale comment too. The gap is narrower than a first glance suggests: it's
the cluster scope of the remaining write verbs on Services/ConfigMaps/
Deployments/PDBs, not raw Secret access.

Evidence

deploy/rbac.yaml:12-80 at upstream/main (5bda5841a64d5d6a6d46062f0c0169e20c7b7b59):

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: praxis-operator
rules:
  ...
  # The operator creates and owns child Services and ConfigMaps.
  - apiGroups: [""]
    resources: ["services", "configmaps"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  # Secrets (TLS material) and Endpoints are only ever read.
  - apiGroups: [""]
    resources: ["secrets", "endpoints"]
    verbs: ["get", "list", "watch"]
  ...
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  # PodDisruptionBudgets keep a data-plane pod serving through a drain.
  - apiGroups: ["policy"]
    resources: ["poddisruptionbudgets"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  ...
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: praxis-operator
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: praxis-operator

There's no per-namespace Role/RoleBinding variant offered, and nothing in
docs/configuration.md or docs/architecture.md explains why cluster scope
(vs. e.g. a Role applied to each namespace containing a managed Gateway)
was chosen.

Risk

A bug in reconciliation logic, or a compromised operator pod, could
create/update/delete a Service, ConfigMap, Deployment, or
PodDisruptionBudget in any namespace in the cluster, not just ones with a
Praxis Gateway. For a generic, cross-namespace Gateway API controller this
may well be a reasonable and even necessary trade-off (the operator can't
know in advance which namespaces will host a Gateway), but that's a
judgment call that should be recorded, not left implicit.

Suggested fix

Pick one and document the decision in docs/configuration.md:

  • Accept as-is, with a short paragraph explaining why cluster scope is
    necessary for a Gateway API controller that must react to Gateway/
    HTTPRoute objects in namespaces it doesn't control in advance.
  • Narrow it, e.g. by reconciling a Role/RoleBinding per namespace that
    actually contains a managed Gateway (mirroring how the child resources
    themselves are already namespace-scoped).

Either way, a one-line rationale comment (matching the style already used for
the Secrets/Endpoints and PDB rules) on the Services/ConfigMaps/Deployments
block would close the documentation gap cheaply.

Severity: Medium

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions