improve RBAC setup for postgres pods

changed the default name, updated helm chart and docs
allow role to use privileged podsecurtitypolicies
This commit is contained in:
Felix Kunde
2020-02-17 11:37:59 +01:00
parent 702a194c41
commit cb96454a56
24 changed files with 382 additions and 129 deletions
+3
View File
@@ -771,6 +771,9 @@ var OperatorConfigCRDResourceValidation = apiextv1beta1.CustomResourceValidation
"pod_service_account_name": {
Type: "string",
},
"pod_service_account_role_definition": {
Type: "string",
},
"pod_service_account_role_binding_definition": {
Type: "string",
},
@@ -43,6 +43,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"`
+1
View File
@@ -45,6 +45,7 @@ type Config struct {
RestConfig *rest.Config
InfrastructureRoles map[string]spec.PgUser // inherited from the controller
PodServiceAccount *v1.ServiceAccount
PodServiceAccountRole *rbacv1.Role
PodServiceAccountRoleBinding *rbacv1.RoleBinding
}
+111 -11
View File
@@ -57,6 +57,7 @@ type Controller struct {
workerLogs map[uint32]ringlog.RingLogger
PodServiceAccount *v1.ServiceAccount
PodServiceAccountRole *rbacv1.Role
PodServiceAccountRoleBinding *rbacv1.RoleBinding
}
@@ -161,11 +162,12 @@ func (c *Controller) initPodServiceAccount() {
if c.opConfig.PodServiceAccountDefinition == "" {
c.opConfig.PodServiceAccountDefinition = `
{ "apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"name": "operator"
}
{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"name": "postgres-pod"
}
}`
}
@@ -175,13 +177,13 @@ func (c *Controller) initPodServiceAccount() {
switch {
case err != nil:
panic(fmt.Errorf("Unable to parse pod service account definition from the operator config map: %v", err))
panic(fmt.Errorf("Unable to parse pod service account definition from the operator configuration: %v", err))
case groupVersionKind.Kind != "ServiceAccount":
panic(fmt.Errorf("pod service account definition in the operator config map defines another type of resource: %v", groupVersionKind.Kind))
panic(fmt.Errorf("pod service account definition in the operator configuration defines another type of resource: %v", groupVersionKind.Kind))
default:
c.PodServiceAccount = obj.(*v1.ServiceAccount)
if c.PodServiceAccount.Name != c.opConfig.PodServiceAccountName {
c.logger.Warnf("in the operator config map, the pod service account name %v does not match the name %v given in the account definition; using the former for consistency", c.opConfig.PodServiceAccountName, c.PodServiceAccount.Name)
c.logger.Warnf("in the operator configuration, the pod service account name %v does not match the name %v given in the account definition; using the former for consistency", c.opConfig.PodServiceAccountName, c.PodServiceAccount.Name)
c.PodServiceAccount.Name = c.opConfig.PodServiceAccountName
}
c.PodServiceAccount.Namespace = ""
@@ -190,6 +192,100 @@ 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"
]
},
{
"apiGroups": [
"extensions"
],
"resources": [
"podsecuritypolicies"
],
"resourceNames": [
"privileged"
],
"verbs": [
"use"
]
}
]
}`, 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
@@ -205,7 +301,7 @@ func (c *Controller) initRoleBinding() {
},
"roleRef": {
"apiGroup": "rbac.authorization.k8s.io",
"kind": "ClusterRole",
"kind": "Role",
"name": "%s"
},
"subjects": [
@@ -223,9 +319,9 @@ func (c *Controller) initRoleBinding() {
switch {
case err != nil:
panic(fmt.Errorf("Unable to parse the definition of the role binding for the pod service account definition from the operator config map: %v", err))
panic(fmt.Errorf("unable to parse the role binding definition from the operator configuration: %v", err))
case groupVersionKind.Kind != "RoleBinding":
panic(fmt.Errorf("role binding definition in the operator config map defines another type of resource: %v", groupVersionKind.Kind))
panic(fmt.Errorf("role binding definition in the operator configuration defines another type of resource: %v", groupVersionKind.Kind))
default:
c.PodServiceAccountRoleBinding = obj.(*rbacv1.RoleBinding)
c.PodServiceAccountRoleBinding.Namespace = ""
@@ -253,6 +349,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()
+1
View File
@@ -45,6 +45,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.CustomPodAnnotations = fromCRD.Kubernetes.CustomPodAnnotations
result.PodServiceAccountName = fromCRD.Kubernetes.PodServiceAccountName
result.PodServiceAccountDefinition = fromCRD.Kubernetes.PodServiceAccountDefinition
result.PodServiceAccountRoleDefinition = fromCRD.Kubernetes.PodServiceAccountRoleDefinition
result.PodServiceAccountRoleBindingDefinition = fromCRD.Kubernetes.PodServiceAccountRoleBindingDefinition
result.PodEnvironmentConfigMap = fromCRD.Kubernetes.PodEnvironmentConfigMap
result.PodTerminateGracePeriod = time.Duration(fromCRD.Kubernetes.PodTerminateGracePeriod)
+45 -21
View File
@@ -493,23 +493,31 @@ func (c *Controller) postgresqlDelete(obj interface{}) {
}
/*
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 {
namespace := event.NewSpec.GetNamespace()
if err := c.createPodServiceAccount(namespace); err != nil {
return fmt.Errorf("could not create pod service account %v : %v", c.opConfig.PodServiceAccountName, err)
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 %v : %v", c.PodServiceAccountRoleBinding.Name, err)
return fmt.Errorf("could not create role binding %q : %v", c.PodServiceAccountRoleBinding.Name, err)
}
return nil
}
@@ -517,19 +525,39 @@ func (c *Controller) submitRBACCredentials(event ClusterEvent) error {
func (c *Controller) createPodServiceAccount(namespace string) error {
podServiceAccountName := c.opConfig.PodServiceAccountName
// get a separate copy of service account
// to prevent a race condition when setting a namespace for many clusters
sa := *c.PodServiceAccount
_, err := c.KubeClient.ServiceAccounts(namespace).Get(podServiceAccountName, metav1.GetOptions{})
if k8sutil.ResourceNotFound(err) {
c.logger.Infof(fmt.Sprintf("creating pod service account in the namespace %v", namespace))
// get a separate copy of service account
// to prevent a race condition when setting a namespace for many clusters
sa := *c.PodServiceAccount
c.logger.Infof(fmt.Sprintf("creating pod service account %q in the %q namespace", podServiceAccountName, namespace))
if _, err = c.KubeClient.ServiceAccounts(namespace).Create(&sa); err != nil {
return fmt.Errorf("cannot deploy the pod service account %v defined in the config map to the %v namespace: %v", podServiceAccountName, namespace, err)
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
}
c.logger.Infof("successfully deployed the pod service account %v to the %v namespace", podServiceAccountName, namespace)
return err
}
func (c *Controller) createRole(namespace string) error {
podServiceAccountRoleName := c.PodServiceAccountRole.Name
// 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).Get(podServiceAccountRoleName, metav1.GetOptions{})
if k8sutil.ResourceNotFound(err) {
c.logger.Infof("creating role %q in the %q namespace", podServiceAccountRoleName, namespace)
_, err = c.KubeClient.Roles(namespace).Create(&role)
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
}
@@ -541,22 +569,18 @@ func (c *Controller) createRoleBindings(namespace string) error {
podServiceAccountName := c.opConfig.PodServiceAccountName
podServiceAccountRoleBindingName := c.PodServiceAccountRoleBinding.Name
// get a separate copy of role binding
// to prevent a race condition when setting a namespace for many clusters
rb := *c.PodServiceAccountRoleBinding
_, err := c.KubeClient.RoleBindings(namespace).Get(podServiceAccountRoleBindingName, metav1.GetOptions{})
if k8sutil.ResourceNotFound(err) {
c.logger.Infof("Creating the role binding %v in the namespace %v", podServiceAccountRoleBindingName, namespace)
// get a separate copy of role binding
// to prevent a race condition when setting a namespace for many clusters
rb := *c.PodServiceAccountRoleBinding
c.logger.Infof("creating the role binding %q in the %q namespace", podServiceAccountRoleBindingName, namespace)
_, err = c.KubeClient.RoleBindings(namespace).Create(&rb)
if err != nil {
return fmt.Errorf("cannot bind the pod service account %q defined in the config map to the cluster role in the %q namespace: %v", podServiceAccountName, namespace, err)
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
}
+6 -6
View File
@@ -91,14 +91,14 @@ type Config struct {
Scalyr
LogicalBackup
WatchedNamespace string `name:"watched_namespace"` // special values: "*" means 'watch all namespaces', the empty string "" means 'watch a namespace where operator is deployed to'
EtcdHost string `name:"etcd_host" default:""` // special values: the empty string "" means Patroni will use K8s as a DCS
DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16"`
Sidecars map[string]string `name:"sidecar_docker_images"`
// default name `operator` enables backward compatibility with the older ServiceAccountName field
PodServiceAccountName string `name:"pod_service_account_name" default:"operator"`
WatchedNamespace string `name:"watched_namespace"` // special values: "*" means 'watch all namespaces', the empty string "" means 'watch a namespace where operator is deployed to'
EtcdHost string `name:"etcd_host" default:""` // special values: the empty string "" means Patroni will use K8s as a DCS
DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16"`
Sidecars map[string]string `name:"sidecar_docker_images"`
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"`
+2
View File
@@ -39,6 +39,7 @@ type KubernetesClient struct {
corev1.NamespacesGetter
corev1.ServiceAccountsGetter
appsv1.StatefulSetsGetter
rbacv1.RolesGetter
rbacv1.RoleBindingsGetter
policyv1beta1.PodDisruptionBudgetsGetter
apiextbeta1.CustomResourceDefinitionsGetter
@@ -103,6 +104,7 @@ func NewFromConfig(cfg *rest.Config) (KubernetesClient, error) {
kubeClient.StatefulSetsGetter = client.AppsV1()
kubeClient.PodDisruptionBudgetsGetter = client.PolicyV1beta1()
kubeClient.RESTClient = client.CoreV1().RESTClient()
kubeClient.RolesGetter = client.RbacV1()
kubeClient.RoleBindingsGetter = client.RbacV1()
kubeClient.CronJobsGetter = client.BatchV1beta1()