# Cluster write access for toolshed's deployer, and only deployer. # # Lives here rather than in toolshed's own Helm chart because these are # cluster-scoped, and toolshed's Application runs in the `webapp` project, # whose clusterResourceWhitelist deliberately allows only Namespace. Widening # that project to permit ClusterRole and ClusterRoleBinding would let *any* # Application in it — every demo app — create cluster-wide RBAC, which is a # privilege-escalation surface in the project that holds ordinary apps. The # narrow fix is to put the two cluster-scoped objects where platform-level # cluster resources already live. # # The ServiceAccount they bind to is namespaced, so it stays in toolshed's # chart alongside the Deployment that uses it. If that ever moves, this # binding's subject has to move with it. # # Scoped to the resource kinds toolshed creates for an app. Not cluster-admin # and not a wildcard: this is the one credential in the system whose # compromise means the cluster, so what it can do should fit on one screen. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: toolshed-deployer labels: app.kubernetes.io/part-of: toolshed rules: # Namespaces are cluster-scoped, and one is created per app. - apiGroups: [""] resources: ["namespaces"] verbs: ["get", "list", "watch", "create", "delete"] # Everything an app needs inside its own namespace. - apiGroups: [""] resources: ["services", "resourcequotas"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # An app's configuration, delivered as a Secret so values never appear in # the pod spec. # # Deliberately without list or watch. Kubernetes RBAC cannot scope a # ClusterRole to a namespace pattern, so this necessarily covers every # namespace — but without list, deployer cannot enumerate the cluster's # secrets, only address ones by a name it already knows. That narrows the # blast radius without removing it: get on a known name still reaches any # secret in the cluster. # # The proper fix, when this has tenants who are not the operator, is a # RoleBinding created per app namespace instead of one ClusterRole. That # needs deployer to hold permission to create RoleBindings, which is its # own escalation path and wants thinking about rather than adding here. - apiGroups: [""] resources: ["secrets"] verbs: ["get", "create", "update", "patch", "delete"] # Read-only. Pods are listed to report why a rollout failed, never changed, # and their output is read so an app's own logs can be shown in the # dashboard without anyone reaching for kubectl. - apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"] - apiGroups: ["apps"] resources: ["deployments"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # The policy that stops one app reaching another. - apiGroups: ["networking.k8s.io"] resources: ["networkpolicies"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # Created only for an app with autoscaling enabled (max replicas set # above min); removed again if it's turned back off. See toolshed's own # internal/deploy.Client.ensureAutoscaler. Added alongside that feature — # keep this file and toolshed's deploy/helm/toolshed/templates/rbac.yaml # in sync, per internal/deploy/kubernetes.go's own package doc warning # that the two are unsynchronized copies in two repositories. - apiGroups: ["autoscaling"] resources: ["horizontalpodautoscalers"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: toolshed-deployer labels: app.kubernetes.io/part-of: toolshed subjects: - kind: ServiceAccount name: toolshed-deployer namespace: toolshed roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: toolshed-deployer