diff --git a/charts/operator/templates/clusterrole.yaml b/charts/operator/templates/clusterrole.yaml new file mode 100644 index 000000000..3e764df4b --- /dev/null +++ b/charts/operator/templates/clusterrole.yaml @@ -0,0 +1,42 @@ +{{- if and (not .Values.kubeconfigSecret.name) .Values.createClusterRole }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "zenith-operator.fullname" . }} + labels: {{ include "zenith-operator.labels" . | nindent 4 }} +rules: + # CRD management (applied at operator startup) + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + # Zenith custom resources watched and reconciled by the operator + - apiGroups: ["zenith.stackhpc.com"] + resources: ["reservations", "clients"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["zenith.stackhpc.com"] + resources: ["reservations/status", "clients/status"] + verbs: ["get", "update", "patch"] + # Secrets for SSH keypairs and client configuration + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + # Services — read-only, used for upstream host resolution + - apiGroups: [""] + resources: ["services"] + verbs: ["get", "list", "watch"] + # ServiceAccounts created for MITM-proxy pods + - apiGroups: [""] + resources: ["serviceaccounts"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + # Deployments for zenith-client pods (created and watched) + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + # ClusterRoleBindings for MITM-proxy ServiceAccounts + # Note: Kubernetes prevents privilege escalation — the operator can only bind roles + # it already holds. To bind cluster-admin to MITM-proxy ServiceAccounts, set + # createClusterRole: false and clusterRoleName: cluster-admin explicitly. + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["clusterrolebindings"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +{{- end }} diff --git a/charts/operator/templates/clusterrolebinding.yaml b/charts/operator/templates/clusterrolebinding.yaml index b2d122133..b69c0d8fd 100644 --- a/charts/operator/templates/clusterrolebinding.yaml +++ b/charts/operator/templates/clusterrolebinding.yaml @@ -12,5 +12,5 @@ subjects: roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: {{ .Values.clusterRoleName }} + name: {{ .Values.clusterRoleName | default (include "zenith-operator.fullname" .) }} {{- end }} diff --git a/charts/operator/tests/__snapshot__/snapshot_test.yaml.snap b/charts/operator/tests/__snapshot__/snapshot_test.yaml.snap index 17fd8ed67..1d855c7dc 100644 --- a/charts/operator/tests/__snapshot__/snapshot_test.yaml.snap +++ b/charts/operator/tests/__snapshot__/snapshot_test.yaml.snap @@ -1,5 +1,106 @@ templated manifests should match snapshot: 1: | + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + labels: + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: zenith-operator + app.kubernetes.io/version: main + helm.sh/chart: zenith-operator-0.1.0 + name: release-name-zenith-operator + rules: + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - list + - watch + - create + - update + - patch + - apiGroups: + - zenith.stackhpc.com + resources: + - reservations + - clients + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - zenith.stackhpc.com + resources: + - reservations/status + - clients/status + verbs: + - get + - update + - patch + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + 2: | apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: @@ -13,12 +114,12 @@ templated manifests should match snapshot: roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: cluster-admin + name: release-name-zenith-operator subjects: - kind: ServiceAccount name: release-name-zenith-operator namespace: NAMESPACE - 2: | + 3: | apiVersion: apps/v1 kind: Deployment metadata: @@ -71,7 +172,7 @@ templated manifests should match snapshot: secretName: release-name-zenith-operator - emptyDir: {} name: tmp - 3: | + 4: | apiVersion: v1 kind: Secret metadata: @@ -85,7 +186,7 @@ templated manifests should match snapshot: stringData: operator.yaml: | defaultImageTag: main - 4: | + 5: | apiVersion: v1 kind: ServiceAccount metadata: diff --git a/charts/operator/values.yaml b/charts/operator/values.yaml index c9267916b..b04f09970 100644 --- a/charts/operator/values.yaml +++ b/charts/operator/values.yaml @@ -10,15 +10,29 @@ kubeconfigSecret: # The key of the kubeconfig file in the secret key: value -# The name of the clusterrole that the operator should be bound to +# When true, a purpose-scoped ClusterRole is created for the operator covering +# only the permissions it needs: zenith CRDs/CRs, secrets, services, service +# accounts, deployments, and clusterrolebindings. # -# Note that in order for the operator to grant permissions to service accounts -# for MITM proxies, the operator itself needs to hold all of those permissions -# In particular, in order for the operator to grant the cluster-admin role -# to service accounts for MITM proxies, it must hold cluster-admin itself +# Set to false when you need to bind a pre-existing role (e.g. cluster-admin +# for MITM-proxy deployments that inject service-account tokens) and supply +# the role name in clusterRoleName below. # -# Only used if kubeconfigSecret.name is not specified -clusterRoleName: cluster-admin +# Only used if kubeconfigSecret.name is not specified. +createClusterRole: true + +# The name of the ClusterRole that the operator ServiceAccount is bound to. +# +# Defaults to the chart fullname (i.e. the scoped ClusterRole created above). +# +# IMPORTANT — privilege escalation: Kubernetes prevents an operator from granting +# a ClusterRole to a ServiceAccount unless the operator itself holds every +# permission in that role. If you use MITM-proxy auth-inject with a role that +# exceeds the scoped ClusterRole (e.g. cluster-admin), you must set this field +# explicitly to that role name and set createClusterRole: false. +# +# Only used if kubeconfigSecret.name is not specified. +clusterRoleName: "" # The image to use for the operator image: