diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index a360da0c6..b40ac0774 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -239,6 +239,9 @@ spec: pod_service_account_name: type: string default: "postgres-pod" + pod_service_account_role_definition: + type: string + default: "" pod_service_account_role_binding_definition: type: string default: "" diff --git a/charts/postgres-operator/templates/clusterrole.yaml b/charts/postgres-operator/templates/clusterrole.yaml index 885bad3f7..82cb22c95 100644 --- a/charts/postgres-operator/templates/clusterrole.yaml +++ b/charts/postgres-operator/templates/clusterrole.yaml @@ -220,11 +220,12 @@ rules: verbs: - get - create -# to create role bindings to the postgres-pod service account +# to create roles and role bindings to the pod service account - apiGroups: - rbac.authorization.k8s.io resources: - rolebindings + - roles verbs: - get - create diff --git a/charts/postgres-operator/values-crd.yaml b/charts/postgres-operator/values-crd.yaml index f3115dc8e..1904dbf7f 100644 --- a/charts/postgres-operator/values-crd.yaml +++ b/charts/postgres-operator/values-crd.yaml @@ -128,6 +128,9 @@ configKubernetes: # service account definition as JSON/YAML string to be used by postgres cluster pods # pod_service_account_definition: "" + # role definition as JSON/YAML string to be used by postgres cluster pods + # pod_service_account_role_definition: "" + # role binding definition as JSON/YAML string to be used by pod service account # pod_service_account_role_binding_definition: "" diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index e8a330d4b..330c4a04d 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -121,6 +121,9 @@ configKubernetes: # service account definition as JSON/YAML string to be used by postgres cluster pods # pod_service_account_definition: "" + # role definition as JSON/YAML string to be used by postgres cluster pods + # pod_service_account_role_definition: "" + # role binding definition as JSON/YAML string to be used by pod service account # pod_service_account_role_binding_definition: "" diff --git a/docs/administrator.md b/docs/administrator.md index 30b612ded..520f9b129 100644 --- a/docs/administrator.md +++ b/docs/administrator.md @@ -212,14 +212,15 @@ kubectl create -f manifests/postgres-operator.yaml kubectl create -f manifests/minimal-postgres-manifest.yaml ``` -### Namespaced service account and role binding - -For each namespace the operator watches it creates (or reads) a service account -and role binding to be used by the Postgres Pods. The service account is bound -to the `postgres-pod` cluster role. The name and definitions of these resources -can be [configured](reference/operator_parameters.md#kubernetes-resources). -Note, that the operator performs **no** further syncing of namespaced service -accounts and role bindings. +### Namespaced service account, role and role binding + +For each namespace the operator watches it creates (or reads) a service account, +a role and a role binding to be used by the Postgres pods. The name and RBAC +definitions can be [configured](reference/operator_parameters.md#kubernetes-resources). +Note, that the operator performs **no** further syncing of these namespaced +resources. Therefore, to manage changes of the pod role it is currently easier +to make `postgres-pod` a [cluster role](../manifests/postgres-pod-rbac.yaml) and +overwrite the `pod_service_account_role_binding_definition` to reference it. ### Give K8s users access to create/list `postgresqls` diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index 5c5850505..7797f1b73 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -189,13 +189,20 @@ configuration they are grouped under the `kubernetes` key. with this parameter. Make sure to provide a valid YAML or JSON string. The default is empty. +* **pod_service_account_role_definition** + operator will try to create a role in the namespace to be used by pod service + account. The internal default definition contains permissions to manage pods + and endpoints necessary for Patroni to work. Therefore, when overwriting the + definition with this parameter make sure to provide sufficient access rights + in a valid YAML/JSON string. The default is empty. + * **pod_service_account_role_binding_definition** - This definition must bind the pod service account to a role with permission - sufficient for the pods to start and for Patroni to access K8s endpoints; - service account on its own lacks any such rights starting with K8s v1.8. If - not explicitly defined by the user, a simple definition that binds the - account to the 'postgres-pod' [cluster role](../../manifests/operator-service-account-rbac.yaml#L198) - will be used. The default is empty. + the created service account and role are referenced with a role binding. When + overwriting its definition with this parameters using a valid YAML/JSON string + check that the specified service account and role either exist in the K8s + cluster or will be created by the operator. While it's possible to also + reference cluster roles, the binding itself can only be of kind `RoleBinding`, + not `ClusterRoleBinding`. The default is empty. * **pod_terminate_grace_period** Postgres pods are [terminated forcefully](https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods) diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index 3788d8b32..5a25cba5b 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -95,7 +95,8 @@ data: pod_role_label: spilo-role # pod_service_account_definition: "" pod_service_account_name: "postgres-pod" - # pod_service_account_role_binding_definition: "" + # pod_service_account_role_definition: "" + # pod_service_account_role_binding_definition: '{"apiVersion":"rbac.authorization.k8s.io/v1","kind":"RoleBinding","metadata":{"name":"postgres-pod"},"roleRef":{"apiGroup":"rbac.authorization.k8s.io","kind": "ClusterRole","name": "postgres-pod"},"subjects":[{"kind": "ServiceAccount","name": "postgres-pod"}]}' pod_terminate_grace_period: 5m # postgres_superuser_teams: "postgres_superusers" # protected_role_names: "admin" diff --git a/manifests/operator-service-account-rbac.yaml b/manifests/operator-service-account-rbac.yaml index f0307f6a0..2f0da9ea7 100644 --- a/manifests/operator-service-account-rbac.yaml +++ b/manifests/operator-service-account-rbac.yaml @@ -195,11 +195,12 @@ rules: verbs: - get - create -# to create role bindings to the postgres-pod service account +# to create roles and role bindings to the pod service account - apiGroups: - rbac.authorization.k8s.io resources: - rolebindings + - roles verbs: - get - create @@ -226,51 +227,3 @@ subjects: - kind: ServiceAccount name: postgres-operator namespace: default - ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: postgres-pod -rules: -# Patroni needs to watch and manage endpoints -- apiGroups: - - "" - resources: - - endpoints - verbs: - - create - - delete - - deletecollection - - get - - list - - patch - - update - - watch -# Patroni needs to watch pods -- apiGroups: - - "" - resources: - - pods - verbs: - - get - - list - - patch - - update - - watch -# to let Patroni create a headless service -- apiGroups: - - "" - resources: - - services - verbs: - - create -# to grant privilege to run privileged pods (not needed by default) -#- apiGroups: -# - extensions -# resources: -# - podsecuritypolicies -# resourceNames: -# - privileged -# verbs: -# - use diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index 7add1b8c6..60ceb56de 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -235,6 +235,9 @@ spec: pod_service_account_name: type: string default: "postgres-pod" + pod_service_account_role_definition: + type: string + default: "" pod_service_account_role_binding_definition: type: string default: "" diff --git a/manifests/postgres-pod-rbac.yaml b/manifests/postgres-pod-rbac.yaml new file mode 100644 index 000000000..ac5badeb8 --- /dev/null +++ b/manifests/postgres-pod-rbac.yaml @@ -0,0 +1,53 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: postgres-pod + namespace: default + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: postgres-pod +rules: +# Patroni needs to watch and manage endpoints +- apiGroups: + - "" + resources: + - endpoints + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch +# Patroni needs to watch pods +- apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - patch + - update + - watch +# to let Patroni create a headless service +- apiGroups: + - "" + resources: + - services + verbs: + - create +# to grant privilege to run privileged pods (not needed by default) +#- apiGroups: +# - extensions +# resources: +# - podsecuritypolicies +# resourceNames: +# - privileged +# verbs: +# - use diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index 96394976d..622978b06 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -69,6 +69,7 @@ configuration: pod_role_label: spilo-role # pod_service_account_definition: "" pod_service_account_name: postgres-pod + # pod_service_account_role_definition: "" # pod_service_account_role_binding_definition: "" pod_terminate_grace_period: 5m secret_name_template: "{username}.{cluster}.credentials.{tprkind}.{tprgroup}" diff --git a/pkg/apis/acid.zalan.do/v1/crds.go b/pkg/apis/acid.zalan.do/v1/crds.go index 02d40342f..d1dd0849d 100644 --- a/pkg/apis/acid.zalan.do/v1/crds.go +++ b/pkg/apis/acid.zalan.do/v1/crds.go @@ -1119,6 +1119,9 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{ "pod_service_account_name": { Type: "string", }, + "pod_service_account_role_definition": { + Type: "string", + }, "pod_service_account_role_binding_definition": { Type: "string", }, diff --git a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go index b55bfa492..881746d9f 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -46,6 +46,7 @@ type KubernetesMetaConfiguration struct { PodServiceAccountName string `json:"pod_service_account_name,omitempty"` // TODO: change it to the proper json PodServiceAccountDefinition string `json:"pod_service_account_definition,omitempty"` + PodServiceAccountRoleDefinition string `json:"pod_service_account_role_definition,omitempty"` PodServiceAccountRoleBindingDefinition string `json:"pod_service_account_role_binding_definition,omitempty"` PodTerminateGracePeriod Duration `json:"pod_terminate_grace_period,omitempty"` SpiloPrivileged bool `json:"spilo_privileged,omitempty"` diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index 16d399865..a2effc623 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -52,6 +52,7 @@ type Config struct { PgTeamMap pgteams.PostgresTeamMap InfrastructureRoles map[string]spec.PgUser // inherited from the controller PodServiceAccount *v1.ServiceAccount + PodServiceAccountRole *rbacv1.Role PodServiceAccountRoleBinding *rbacv1.RoleBinding } diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 0c29275e6..288d11995 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -71,6 +71,7 @@ type Controller struct { workerLogs map[uint32]ringlog.RingLogger PodServiceAccount *v1.ServiceAccount + PodServiceAccountRole *rbacv1.Role PodServiceAccountRoleBinding *rbacv1.RoleBinding } @@ -248,6 +249,86 @@ func (c *Controller) initPodServiceAccount() { // actual service accounts are deployed at the time of Postgres/Spilo cluster creation } +func (c *Controller) initRole() { + + // service account on its own lacks any rights starting with k8s v1.8 + // operator binds it to the namespaced role with sufficient privileges + if c.opConfig.PodServiceAccountRoleDefinition == "" { + c.opConfig.PodServiceAccountRoleDefinition = fmt.Sprintf(` + { + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "Role", + "metadata": { + "name": "%s" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resources": [ + "endpoints" + ], + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "pods" + ], + "verbs": [ + "get", + "list", + "patch", + "update", + "watch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "services" + ], + "verbs": [ + "create" + ] + } + ] + }`, c.PodServiceAccount.Name) + } + c.logger.Info("Parse roles") + // re-uses k8s internal parsing. See k8s client-go issue #193 for explanation + decode := scheme.Codecs.UniversalDeserializer().Decode + obj, groupVersionKind, err := decode([]byte(c.opConfig.PodServiceAccountRoleDefinition), nil, nil) + + switch { + case err != nil: + panic(fmt.Errorf("unable to parse the role definition from the operator configuration: %v", err)) + case groupVersionKind.Kind != "Role": + panic(fmt.Errorf("role definition in the operator configuration defines another type of resource: %v", groupVersionKind.Kind)) + default: + c.PodServiceAccountRole = obj.(*rbacv1.Role) + c.PodServiceAccountRole.Namespace = "" + c.logger.Info("successfully parsed") + + } + + // actual roles bindings are deployed at the time of Postgres/Spilo cluster creation +} + func (c *Controller) initRoleBinding() { // service account on its own lacks any rights starting with k8s v1.8 @@ -263,7 +344,7 @@ func (c *Controller) initRoleBinding() { }, "roleRef": { "apiGroup": "rbac.authorization.k8s.io", - "kind": "ClusterRole", + "kind": "Role", "name": "%s" }, "subjects": [ @@ -320,6 +401,10 @@ func (c *Controller) initController() { } c.initPodServiceAccount() c.initRoleBinding() + // init role only if binding references a role + if c.PodServiceAccountRoleBinding != nil && c.PodServiceAccountRoleBinding.RoleRef.Kind == "Role" { + c.initRole() + } c.modifyConfigFromEnvironment() diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 16fb05004..8d5101167 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -58,6 +58,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.CustomPodAnnotations = fromCRD.Kubernetes.CustomPodAnnotations result.PodServiceAccountName = util.Coalesce(fromCRD.Kubernetes.PodServiceAccountName, "postgres-pod") result.PodServiceAccountDefinition = fromCRD.Kubernetes.PodServiceAccountDefinition + result.PodServiceAccountRoleDefinition = fromCRD.Kubernetes.PodServiceAccountRoleDefinition result.PodServiceAccountRoleBindingDefinition = fromCRD.Kubernetes.PodServiceAccountRoleBindingDefinition result.PodEnvironmentConfigMap = fromCRD.Kubernetes.PodEnvironmentConfigMap result.PodEnvironmentSecret = fromCRD.Kubernetes.PodEnvironmentSecret diff --git a/pkg/controller/postgresql.go b/pkg/controller/postgresql.go index 0fe0c1120..8f0828b0e 100644 --- a/pkg/controller/postgresql.go +++ b/pkg/controller/postgresql.go @@ -547,12 +547,12 @@ func (c *Controller) postgresqlCheck(obj interface{}) *acidv1.Postgresql { } /* - Ensures the pod service account and role bindings exists in a namespace + Ensures the pod service account, role and role bindings exists in a namespace before a PG cluster is created there so that a user does not have to deploy these credentials manually. StatefulSets require the service account to create pods; Patroni requires relevant RBAC bindings to access endpoints. - The operator does not sync accounts/role bindings after creation. + The operator does not sync accounts/roles/role bindings after creation. */ func (c *Controller) submitRBACCredentials(event ClusterEvent) error { @@ -562,6 +562,14 @@ func (c *Controller) submitRBACCredentials(event ClusterEvent) error { return fmt.Errorf("could not create pod service account %q : %v", c.opConfig.PodServiceAccountName, err) } + // create role only if binding references a role + // if not role is empty and we rely on an existing cluster role + if c.PodServiceAccountRole != nil { + if err := c.createRole(namespace); err != nil { + return fmt.Errorf("could not create role %q : %v", c.PodServiceAccountRole.Name, err) + } + } + if err := c.createRoleBindings(namespace); err != nil { return fmt.Errorf("could not create role binding %q : %v", c.PodServiceAccountRoleBinding.Name, err) } @@ -573,7 +581,6 @@ func (c *Controller) createPodServiceAccount(namespace string) error { podServiceAccountName := c.opConfig.PodServiceAccountName _, err := c.KubeClient.ServiceAccounts(namespace).Get(context.TODO(), podServiceAccountName, metav1.GetOptions{}) if k8sutil.ResourceNotFound(err) { - c.logger.Infof(fmt.Sprintf("creating pod service account %q in the %q namespace", podServiceAccountName, namespace)) // get a separate copy of service account @@ -582,7 +589,6 @@ func (c *Controller) createPodServiceAccount(namespace string) error { if _, err = c.KubeClient.ServiceAccounts(namespace).Create(context.TODO(), &sa, metav1.CreateOptions{}); err != nil { return fmt.Errorf("cannot deploy the pod service account %q defined in the configuration to the %q namespace: %v", podServiceAccountName, namespace, err) } - c.logger.Infof("successfully deployed the pod service account %q to the %q namespace", podServiceAccountName, namespace) } else if k8sutil.ResourceAlreadyExists(err) { return nil @@ -591,6 +597,29 @@ func (c *Controller) createPodServiceAccount(namespace string) error { return err } +func (c *Controller) createRole(namespace string) error { + + podServiceAccountRoleName := c.PodServiceAccountRole.Name + + _, err := c.KubeClient.Roles(namespace).Get(context.TODO(), podServiceAccountRoleName, metav1.GetOptions{}) + if k8sutil.ResourceNotFound(err) { + c.logger.Infof("creating role %q in the %q namespace", podServiceAccountRoleName, namespace) + + // get a separate copy of the role + // to prevent a race condition when setting a namespace for many clusters + role := *c.PodServiceAccountRole + _, err = c.KubeClient.Roles(namespace).Create(context.TODO(), &role, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("cannot create role %q in the %q namespace: %v", podServiceAccountRoleName, namespace, err) + } + c.logger.Infof("successfully deployed role %q to the %q namespace", podServiceAccountRoleName, namespace) + } else if k8sutil.ResourceAlreadyExists(err) { + return nil + } + + return err +} + func (c *Controller) createRoleBindings(namespace string) error { podServiceAccountName := c.opConfig.PodServiceAccountName @@ -598,7 +627,6 @@ func (c *Controller) createRoleBindings(namespace string) error { _, err := c.KubeClient.RoleBindings(namespace).Get(context.TODO(), podServiceAccountRoleBindingName, metav1.GetOptions{}) if k8sutil.ResourceNotFound(err) { - c.logger.Infof("Creating the role binding %q in the %q namespace", podServiceAccountRoleBindingName, namespace) // get a separate copy of role binding @@ -608,9 +636,7 @@ func (c *Controller) createRoleBindings(namespace string) error { if err != nil { return fmt.Errorf("cannot bind the pod service account %q defined in the configuration to the cluster role in the %q namespace: %v", podServiceAccountName, namespace, err) } - c.logger.Infof("successfully deployed the role binding for the pod service account %q to the %q namespace", podServiceAccountName, namespace) - } else if k8sutil.ResourceAlreadyExists(err) { return nil } diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 1d8e37bd2..107ea9360 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -156,6 +156,7 @@ type Config struct { PodServiceAccountName string `name:"pod_service_account_name" default:"postgres-pod"` // value of this string must be valid JSON or YAML; see initPodServiceAccount PodServiceAccountDefinition string `name:"pod_service_account_definition" default:""` + PodServiceAccountRoleDefinition string `name:"pod_service_account_role_definition" default:""` PodServiceAccountRoleBindingDefinition string `name:"pod_service_account_role_binding_definition" default:""` MasterPodMoveTimeout time.Duration `name:"master_pod_move_timeout" default:"20m"` DbHostedZone string `name:"db_hosted_zone" default:"db.example.com"` diff --git a/pkg/util/k8sutil/k8sutil.go b/pkg/util/k8sutil/k8sutil.go index dd6ec1e8b..a21434d45 100644 --- a/pkg/util/k8sutil/k8sutil.go +++ b/pkg/util/k8sutil/k8sutil.go @@ -51,6 +51,7 @@ type KubernetesClient struct { corev1.EventsGetter appsv1.StatefulSetsGetter appsv1.DeploymentsGetter + rbacv1.RolesGetter rbacv1.RoleBindingsGetter policyv1beta1.PodDisruptionBudgetsGetter apiextv1.CustomResourceDefinitionsGetter @@ -147,6 +148,7 @@ func NewFromConfig(cfg *rest.Config) (KubernetesClient, error) { kubeClient.DeploymentsGetter = client.AppsV1() kubeClient.PodDisruptionBudgetsGetter = client.PolicyV1beta1() kubeClient.RESTClient = client.CoreV1().RESTClient() + kubeClient.RolesGetter = client.RbacV1() kubeClient.RoleBindingsGetter = client.RbacV1() kubeClient.CronJobsGetter = client.BatchV1beta1() kubeClient.EventsGetter = client.CoreV1()