diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index abe60a1d8..72ea0d42a 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -122,6 +122,12 @@ spec: users: type: object properties: + enable_password_rotation: + type: boolean + default: false + password_rotation_interval: + type: string + default: "90d" replication_username: type: string default: standby diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index 932bd60ca..379f8b92d 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -44,6 +44,7 @@ data: # enable_init_containers: "true" # enable_lazy_spilo_upgrade: "false" enable_master_load_balancer: "false" + enable_password_rotation: "true" enable_pgversion_env_var: "true" # enable_pod_antiaffinity: "false" # enable_pod_disruption_budget: "true" @@ -91,6 +92,7 @@ data: # pam_configuration: | # https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees # pam_role_name: zalandos + password_rotation_interval: 10m pdb_name_format: "postgres-{cluster}-pdb" # pod_antiaffinity_topology_key: "kubernetes.io/hostname" pod_deletion_wait_timeout: 10m diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml index d4b1a2996..05012642d 100644 --- a/manifests/operatorconfiguration.crd.yaml +++ b/manifests/operatorconfiguration.crd.yaml @@ -120,6 +120,12 @@ spec: users: type: object properties: + enable_password_rotation: + type: boolean + default: false + password_rotation_interval: + type: string + default: "90d" replication_username: type: string default: standby diff --git a/manifests/postgres-operator.yaml b/manifests/postgres-operator.yaml index eddd44650..899112eb2 100644 --- a/manifests/postgres-operator.yaml +++ b/manifests/postgres-operator.yaml @@ -19,7 +19,7 @@ spec: serviceAccountName: postgres-operator containers: - name: postgres-operator - image: registry.opensource.zalan.do/acid/postgres-operator:v1.7.1 + image: registry.opensource.zalan.do/acid/postgres-operator:v1.7.1-16-gfe340192-dirty imagePullPolicy: IfNotPresent resources: requests: diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index 2ad74b1e4..cef3c4821 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -25,6 +25,8 @@ configuration: # protocol: TCP workers: 8 users: + enable_password_rotation: false + password_rotation_interval: 90d replication_username: standby super_username: postgres major_version_upgrade: 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 a1dee6bff..862b60ec5 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -37,8 +37,10 @@ type OperatorConfigurationList struct { // PostgresUsersConfiguration defines the system users of Postgres. type PostgresUsersConfiguration struct { - SuperUsername string `json:"super_username,omitempty"` - ReplicationUsername string `json:"replication_username,omitempty"` + SuperUsername string `json:"super_username,omitempty"` + ReplicationUsername string `json:"replication_username,omitempty"` + EnablePasswordRotation bool `json:"enable_password_rotation,omitempty"` + PasswordRotationInterval Duration `json:"password_rotation_interval,omitempty"` } // MajorVersionUpgradeConfiguration defines how to execute major version upgrades of Postgres. diff --git a/pkg/cluster/database.go b/pkg/cluster/database.go index aa3a5e3be..b6a6bc37f 100644 --- a/pkg/cluster/database.go +++ b/pkg/cluster/database.go @@ -17,15 +17,39 @@ import ( ) const ( - getUserSQL = `SELECT a.rolname, COALESCE(a.rolpassword, ''), a.rolsuper, a.rolinherit, - a.rolcreaterole, a.rolcreatedb, a.rolcanlogin, s.setconfig, - ARRAY(SELECT b.rolname - FROM pg_catalog.pg_auth_members m - JOIN pg_catalog.pg_authid b ON (m.roleid = b.oid) - WHERE m.member = a.oid) as memberof - FROM pg_catalog.pg_authid a LEFT JOIN pg_db_role_setting s ON (a.oid = s.setrole AND s.setdatabase = 0::oid) - WHERE a.rolname = ANY($1) - ORDER BY 1;` + getUserSQL = `WITH dbowners AS ( + SELECT DISTINCT pg_catalog.pg_get_userbyid(datdba) AS owner + FROM pg_database + WHERE datname NOT IN ('postgres', 'template0', 'template1') + ), roles AS ( + SELECT a.rolname, COALESCE(a.rolpassword, '') AS rolpassword, a.rolsuper, a.rolinherit, + a.rolcreaterole, a.rolcreatedb, a.rolcanlogin, s.setconfig, + ARRAY(SELECT b.rolname + FROM pg_catalog.pg_auth_members m + JOIN pg_catalog.pg_authid b ON (m.roleid = b.oid) + WHERE m.member = a.oid) as memberof + FROM pg_catalog.pg_authid a + LEFT JOIN pg_catalog.pg_db_role_setting s ON (a.oid = s.setrole AND s.setdatabase = 0::oid) + WHERE a.rolname = ANY($1) + ORDER BY 1 + ) + SELECT r.rolname, r.rolpassword, r.rolsuper, r.rolinherit, + r.rolcreaterole, r.rolcreatedb, r.rolcanlogin, r.setconfig, + r.memberof, + o.owner IS NOT NULL OR r.rolname LIKE '%_owner' AS is_owner + FROM roles r + LEFT JOIN dbowners o ON o.owner = r.rolname OR o.owner = ANY (r.memberof) + ORDER BY 1;` + + /*`SELECT a.rolname, COALESCE(a.rolpassword, ''), a.rolsuper, a.rolinherit, + a.rolcreaterole, a.rolcreatedb, a.rolcanlogin, s.setconfig, + ARRAY(SELECT b.rolname + FROM pg_catalog.pg_auth_members m + JOIN pg_catalog.pg_authid b ON (m.roleid = b.oid) + WHERE m.member = a.oid) as memberof + FROM pg_catalog.pg_authid a LEFT JOIN pg_db_role_setting s ON (a.oid = s.setrole AND s.setdatabase = 0::oid) + WHERE a.rolname = ANY($1) + ORDER BY 1;`*/ getDatabasesSQL = `SELECT datname, pg_get_userbyid(datdba) AS owner FROM pg_database;` getSchemasSQL = `SELECT n.nspname AS dbschema FROM pg_catalog.pg_namespace n @@ -198,10 +222,10 @@ func (c *Cluster) readPgUsersFromDatabase(userNames []string) (users spec.PgUser rolname, rolpassword string rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin bool roloptions, memberof []string - roldeleted bool + rolowner, roldeleted bool ) err := rows.Scan(&rolname, &rolpassword, &rolsuper, &rolinherit, - &rolcreaterole, &rolcreatedb, &rolcanlogin, pq.Array(&roloptions), pq.Array(&memberof)) + &rolcreaterole, &rolcreatedb, &rolcanlogin, pq.Array(&roloptions), pq.Array(&memberof), &rolowner) if err != nil { return nil, fmt.Errorf("error when processing user rows: %v", err) } @@ -221,7 +245,7 @@ func (c *Cluster) readPgUsersFromDatabase(userNames []string) (users spec.PgUser roldeleted = true } - users[rolname] = spec.PgUser{Name: rolname, Password: rolpassword, Flags: flags, MemberOf: memberof, Parameters: parameters, Deleted: roldeleted} + users[rolname] = spec.PgUser{Name: rolname, Password: rolpassword, Flags: flags, MemberOf: memberof, Parameters: parameters, IsOwner: rolowner, Deleted: roldeleted} } return users, nil diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index 2df168a6e..bc77ec6be 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -613,8 +613,9 @@ func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration(pod *v1.Pod, patroniC func (c *Cluster) syncSecrets() error { var ( - err error - secret *v1.Secret + err error + secret *v1.Secret + nextRotationDate time.Time ) c.logger.Info("syncing secrets") c.setProcessName("syncing secrets") @@ -631,11 +632,12 @@ func (c *Cluster) syncSecrets() error { if secret, err = c.KubeClient.Secrets(secretSpec.Namespace).Get(context.TODO(), secretSpec.Name, metav1.GetOptions{}); err != nil { return fmt.Errorf("could not get current secret: %v", err) } - if secretUsername != string(secret.Data["username"]) { + username := string(secret.Data["username"]) + if secretUsername != username { c.logger.Errorf("secret %s does not contain the role %s", secretSpec.Name, secretUsername) continue } - c.Secrets[secret.UID] = secret + c.logger.Debugf("secret %s already exists, fetching its password", util.NameFromMeta(secret.ObjectMeta)) if secretUsername == c.systemUsers[constants.SuperuserKeyName].Name { secretUsername = constants.SuperuserKeyName @@ -647,10 +649,41 @@ func (c *Cluster) syncSecrets() error { userMap = c.pgUsers } pwdUser := userMap[secretUsername] - // if this secret belongs to the infrastructure role and the password has changed - replace it in the secret - if pwdUser.Password != string(secret.Data["password"]) && - pwdUser.Origin == spec.RoleOriginInfrastructure { + // if password rotation is enabled update password and username if rotation interval has been passed + if c.OpConfig.EnablePasswordRotation && pwdUser.Origin != spec.RoleOriginInfrastructure && !pwdUser.IsOwner { // || c.Spec.InPlacePasswordRotation[secretUsername] { + err = json.Unmarshal(secret.Data["nextRotation"], &nextRotationDate) + if err != nil { + c.logger.Warningf("could not read rotation date of secret %s", secretSpec.Name) + nextRotationDate = time.Now() + } + + currentTime := time.Now() + if currentTime.After(nextRotationDate) { + //if !c.Spec.InPlacePasswordRotation[secretUsername] { + newRotationUsername := secretUsername + "_" + currentTime.Format("060102") + pwdUser.Name = newRotationUsername + pwdUser.MemberOf = []string{secretUsername} + pgSyncRequests := c.userSyncStrategy.ProduceSyncRequests(spec.PgUserMap{}, map[string]spec.PgUser{newRotationUsername: pwdUser}) + if err = c.userSyncStrategy.ExecuteSyncRequests(pgSyncRequests, c.pgDb); err != nil { + return fmt.Errorf("error executing sync statements: %v", err) + } + secret.Data["username"] = []byte(newRotationUsername) + //} + secret.Data["password"] = []byte(util.RandomPassword(constants.PasswordLength)) + secret.Data["nextRotation"] = []byte(currentTime.Add(c.OpConfig.PasswordRotationInterval).Format("2006-01-02 15:04:05")) + + c.logger.Debugf("updating the secret %s due to password rotation", secretSpec.Name) + if _, err = c.KubeClient.Secrets(secretSpec.Namespace).Update(context.TODO(), secretSpec, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("could not update secret %q: %v", secretUsername, err) + } + } + } + + c.Secrets[secret.UID] = secret + + // if this secret belongs to the infrastructure role and the password has changed - replace it in the secret + if pwdUser.Password != string(secret.Data["password"]) && pwdUser.Origin == spec.RoleOriginInfrastructure { c.logger.Debugf("updating the secret %s from the infrastructure roles", secretSpec.Name) if _, err = c.KubeClient.Secrets(secretSpec.Namespace).Update(context.TODO(), secretSpec, metav1.UpdateOptions{}); err != nil { return fmt.Errorf("could not update infrastructure role secret for role %q: %v", secretUsername, err) diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 2f5261cd2..a361bba0a 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -54,6 +54,8 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur // user config result.SuperUsername = util.Coalesce(fromCRD.PostgresUsersConfiguration.SuperUsername, "postgres") result.ReplicationUsername = util.Coalesce(fromCRD.PostgresUsersConfiguration.ReplicationUsername, "standby") + result.EnablePasswordRotation = fromCRD.PostgresUsersConfiguration.EnablePasswordRotation + result.PasswordRotationInterval = util.CoalesceDuration(time.Duration(fromCRD.PostgresUsersConfiguration.PasswordRotationInterval), "90d") // major version upgrade config result.MajorVersionUpgradeMode = util.Coalesce(fromCRD.MajorVersionUpgrade.MajorVersionUpgradeMode, "off") diff --git a/pkg/spec/types.go b/pkg/spec/types.go index 533aae79f..3b996b6ff 100644 --- a/pkg/spec/types.go +++ b/pkg/spec/types.go @@ -55,6 +55,7 @@ type PgUser struct { MemberOf []string `yaml:"inrole"` Parameters map[string]string `yaml:"db_parameters"` AdminRole string `yaml:"admin_role"` + IsOwner bool `yaml:"is_owner"` Deleted bool `yaml:"deleted"` } diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index bb77e6231..c88ec52b4 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -99,6 +99,8 @@ type Auth struct { InfrastructureRolesDefs string `name:"infrastructure_roles_secrets"` SuperUsername string `name:"super_username" default:"postgres"` ReplicationUsername string `name:"replication_username" default:"standby"` + EnablePasswordRotation bool `name:"enable_password_rotation" default:"false"` + PasswordRotationInterval time.Duration `name:"password_rotation_interval" default:"90d"` } // Scalyr holds the configuration for the Scalyr Agent sidecar for log shipping: