# 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"] # A one-off command run against an app executes in a throwaway Job built # from the app's own image — see internal/deploy.Client.RunCommand. Delete # is needed because the Job is removed once its output has been captured; # without it they accumulate one per command forever. - apiGroups: ["batch"] resources: ["jobs"] verbs: ["get", "list", "watch", "create", "delete"] # An app with persistent storage is a StatefulSet instead of a Deployment # (internal/deploy.Client.ensureStatefulSet). Delete is needed on both: # switching an app between stateless and stateful must remove whichever # controller it no longer is, or the two fight over the same pods. - apiGroups: ["apps"] resources: ["statefulsets"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # Read-only. The claims themselves are created by the StatefulSet's own # volumeClaimTemplates, never directly here — this is only to observe # them, and deleting one would destroy an app's data. - apiGroups: [""] resources: ["persistentvolumeclaims"] verbs: ["get", "list", "watch"] # 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 --- # A custom domain's Ingress and TLS secret live in the gateway's own # namespace (toolshed, same as above) — never an app's namespace. Scoped # with a namespaced Role/RoleBinding rather than widening the ClusterRole # above: Ingress management here only ever targets this one fixed # namespace, unlike the per-app namespaces the ClusterRole necessarily # spans. Added alongside toolshed's custom-domains feature — see # internal/deploy.Client.EnsureDomainIngress/DomainCertReady/ # RemoveDomainIngress and this file's own header comment about keeping it # and deploy/helm/toolshed/templates/rbac.yaml in sync. apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: toolshed-deployer-ingress namespace: toolshed labels: app.kubernetes.io/part-of: toolshed rules: - apiGroups: ["networking.k8s.io"] resources: ["ingresses"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # Reads the TLS secret cert-manager's ingress-shim populates for a domain # Ingress, and deletes it (and the Ingress above) when a domain is # removed. Never create/update — cert-manager, not deployer, writes this # secret. - apiGroups: [""] resources: ["secrets"] verbs: ["get", "list", "watch", "delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: toolshed-deployer-ingress namespace: toolshed labels: app.kubernetes.io/part-of: toolshed subjects: - kind: ServiceAccount name: toolshed-deployer namespace: toolshed roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: toolshed-deployer-ingress