add user retention
This commit is contained in:
parent
ab9aff3775
commit
08542711e0
|
|
@ -128,6 +128,9 @@ spec:
|
||||||
password_rotation_interval:
|
password_rotation_interval:
|
||||||
type: integer
|
type: integer
|
||||||
default: 90
|
default: 90
|
||||||
|
password_rotation_user_retention:
|
||||||
|
type: integer
|
||||||
|
default: 180
|
||||||
replication_username:
|
replication_username:
|
||||||
type: string
|
type: string
|
||||||
default: standby
|
default: standby
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,62 @@ that are aggregated into the K8s [default roles](https://kubernetes.io/docs/refe
|
||||||
|
|
||||||
For Helm deployments setting `rbac.createAggregateClusterRoles: true` adds these clusterroles to the deployment.
|
For Helm deployments setting `rbac.createAggregateClusterRoles: true` adds these clusterroles to the deployment.
|
||||||
|
|
||||||
|
## Password rotation in K8s secrets
|
||||||
|
|
||||||
|
The operator regularly updates credentials in the K8s secrets if the
|
||||||
|
`enable_password_rotation` option is set to `true` in the configuration.
|
||||||
|
It happens only for LOGIN roles with an associated secret (manifest roles,
|
||||||
|
default user from `preparedDatabases`, system users). Furthermore, there
|
||||||
|
the following roles are excluded:
|
||||||
|
|
||||||
|
1. Infrastructure role secrets since rotation should happen by the infrastructure.
|
||||||
|
2. Team API roles that connect via OAuth2 and JWT token. Rotation should be provided by the infrastructure + there is even no secret for these roles
|
||||||
|
3. Database owners and members of owners, since ownership can not be inherited.
|
||||||
|
|
||||||
|
The interval of days can be set with `password_rotation_interval` (default
|
||||||
|
`90` = 90 days, minimum 1). On each rotation the user name and password values
|
||||||
|
are replaced in the secret. They belong to a newly created user named after
|
||||||
|
the original role plus rotation date in YYMMDD format. All priviliges are
|
||||||
|
inherited meaning that migration scripts continue to apply grants/revokes
|
||||||
|
against the original role. The timestamp of the next rotation is written to
|
||||||
|
the secret as well.
|
||||||
|
|
||||||
|
Pods still using the previous secret values in memory continue to connect to
|
||||||
|
the database since the password of the corresponding user is not replaced.
|
||||||
|
However, a retention policy can be configured for created roles by password
|
||||||
|
rotation with `password_rotation_user_retention`. The operator will ensure
|
||||||
|
that this period is at least twice as long as the configured rotation
|
||||||
|
interval, hence the default of `180` = 180 days.
|
||||||
|
|
||||||
|
### Password rotation for single roles
|
||||||
|
|
||||||
|
From the configuration, password rotation is enabled for all secrets with the
|
||||||
|
mentioned exceptions. If you wish to first test rotation for a single user (or
|
||||||
|
just have it enabled only for a few secrets) you can specify it in the cluster
|
||||||
|
manifest. The rotation and retention intervals can only be configured globally.
|
||||||
|
|
||||||
|
```
|
||||||
|
spec:
|
||||||
|
usersWithSecretRotation: "foo_user,bar_reader_user"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Password replacement without extra roles
|
||||||
|
|
||||||
|
For some use cases where the secret is only used rarely - think of a `flyway`
|
||||||
|
user running a migration script on pod start - we do not need to create extra
|
||||||
|
database roles but can replace only the password in the K8s secret. This type
|
||||||
|
of rotation cannot be configured globally but specified in the cluster
|
||||||
|
manifest:
|
||||||
|
|
||||||
|
```
|
||||||
|
spec:
|
||||||
|
usersWithInPlaceSecretRotation: "flyway,bar_owner_user"
|
||||||
|
```
|
||||||
|
|
||||||
|
This would be the recommended option to enable rotation in secrets of database
|
||||||
|
owners, But only if they are not used as application users for regular read
|
||||||
|
and write operation.
|
||||||
|
|
||||||
## Use taints and tolerations for dedicated PostgreSQL nodes
|
## Use taints and tolerations for dedicated PostgreSQL nodes
|
||||||
|
|
||||||
To ensure Postgres pods are running on nodes without any other application pods,
|
To ensure Postgres pods are running on nodes without any other application pods,
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,28 @@ under the `users` key.
|
||||||
Postgres username used for replication between instances. The default is
|
Postgres username used for replication between instances. The default is
|
||||||
`standby`.
|
`standby`.
|
||||||
|
|
||||||
|
* **enable_password_rotation**
|
||||||
|
For all LOGIN roles that are not database owners the Operator can rotate
|
||||||
|
credentials in the corresponding K8s secrets by replacing the username and
|
||||||
|
password. This means, new users will be added on each rotation inheriting
|
||||||
|
all priviliges from the original roles. The rotation date in the YYMMDD is
|
||||||
|
appended to the new user names. The timestamp of the next rotation is
|
||||||
|
written to the secret. The default is `false`.
|
||||||
|
|
||||||
|
* **password_rotation_interval**
|
||||||
|
If password rotation is enabled (either from config or cluster manifest) the
|
||||||
|
interval can be configured with this parameter. The measure is in days which
|
||||||
|
means daily rotation (`1`) is the most frequent interval possible.
|
||||||
|
Default is `90`.
|
||||||
|
|
||||||
|
* **password_rotation_user_retention**
|
||||||
|
To avoid an ever growing amount of new users due to password rotation the
|
||||||
|
operator will remove the created users again after a certain amount of days
|
||||||
|
has passed. The number can be configured with this parameter. However, the
|
||||||
|
operator will check that the retention policy is at least twice as long as
|
||||||
|
the rotation interval and update to this minimum in case it is not.
|
||||||
|
Default is `180`.
|
||||||
|
|
||||||
## Major version upgrades
|
## Major version upgrades
|
||||||
|
|
||||||
Parameters configuring automatic major version upgrades. In a
|
Parameters configuring automatic major version upgrades. In a
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ spec:
|
||||||
zalando:
|
zalando:
|
||||||
- superuser
|
- superuser
|
||||||
- createdb
|
- createdb
|
||||||
|
foo_user: []
|
||||||
|
# usersWithSecretRotation: "foo_user"
|
||||||
|
# usersWithInPlaceSecretRotation: "flyway,bar_owner_user"
|
||||||
enableMasterLoadBalancer: false
|
enableMasterLoadBalancer: false
|
||||||
enableReplicaLoadBalancer: false
|
enableReplicaLoadBalancer: false
|
||||||
enableConnectionPooler: false # enable/disable connection pooler deployment
|
enableConnectionPooler: false # enable/disable connection pooler deployment
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ data:
|
||||||
# https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees
|
# https://info.example.com/oauth2/tokeninfo?access_token= uid realm=/employees
|
||||||
# pam_role_name: zalandos
|
# pam_role_name: zalandos
|
||||||
# password_rotation_interval: "90"
|
# password_rotation_interval: "90"
|
||||||
|
# password_rotation_user_retention: "180"
|
||||||
pdb_name_format: "postgres-{cluster}-pdb"
|
pdb_name_format: "postgres-{cluster}-pdb"
|
||||||
# pod_antiaffinity_topology_key: "kubernetes.io/hostname"
|
# pod_antiaffinity_topology_key: "kubernetes.io/hostname"
|
||||||
pod_deletion_wait_timeout: 10m
|
pod_deletion_wait_timeout: 10m
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,9 @@ spec:
|
||||||
password_rotation_interval:
|
password_rotation_interval:
|
||||||
type: integer
|
type: integer
|
||||||
default: 90
|
default: 90
|
||||||
|
password_rotation_user_retention:
|
||||||
|
type: integer
|
||||||
|
default: 180
|
||||||
replication_username:
|
replication_username:
|
||||||
type: string
|
type: string
|
||||||
default: standby
|
default: standby
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ configuration:
|
||||||
users:
|
users:
|
||||||
enable_password_rotation: false
|
enable_password_rotation: false
|
||||||
password_rotation_interval: 90
|
password_rotation_interval: 90
|
||||||
|
password_rotation_user_retention: 180
|
||||||
replication_username: standby
|
replication_username: standby
|
||||||
super_username: postgres
|
super_username: postgres
|
||||||
major_version_upgrade:
|
major_version_upgrade:
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,11 @@ type OperatorConfigurationList struct {
|
||||||
|
|
||||||
// PostgresUsersConfiguration defines the system users of Postgres.
|
// PostgresUsersConfiguration defines the system users of Postgres.
|
||||||
type PostgresUsersConfiguration struct {
|
type PostgresUsersConfiguration struct {
|
||||||
SuperUsername string `json:"super_username,omitempty"`
|
SuperUsername string `json:"super_username,omitempty"`
|
||||||
ReplicationUsername string `json:"replication_username,omitempty"`
|
ReplicationUsername string `json:"replication_username,omitempty"`
|
||||||
EnablePasswordRotation bool `json:"enable_password_rotation,omitempty"`
|
EnablePasswordRotation bool `json:"enable_password_rotation,omitempty"`
|
||||||
PasswordRotationInterval uint32 `json:"password_rotation_interval,omitempty"`
|
PasswordRotationInterval uint32 `json:"password_rotation_interval,omitempty"`
|
||||||
|
PasswordRotationUserRetention uint32 `json:"password_rotation_user_retention,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MajorVersionUpgradeConfiguration defines how to execute major version upgrades of Postgres.
|
// MajorVersionUpgradeConfiguration defines how to execute major version upgrades of Postgres.
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,11 @@ type PostgresSpec struct {
|
||||||
// load balancers' source ranges are the same for master and replica services
|
// load balancers' source ranges are the same for master and replica services
|
||||||
AllowedSourceRanges []string `json:"allowedSourceRanges"`
|
AllowedSourceRanges []string `json:"allowedSourceRanges"`
|
||||||
|
|
||||||
|
Users map[string]UserFlags `json:"users,omitempty"`
|
||||||
|
UsersWithSecretRotation []string `json:"usersWithSecretRotation,omitempty"`
|
||||||
|
UsersWithInPlaceSecretRotation []string `json:"usersWithInPlaceSecretRotation,omitempty"`
|
||||||
|
|
||||||
NumberOfInstances int32 `json:"numberOfInstances"`
|
NumberOfInstances int32 `json:"numberOfInstances"`
|
||||||
Users map[string]UserFlags `json:"users,omitempty"`
|
|
||||||
MaintenanceWindows []MaintenanceWindow `json:"maintenanceWindows,omitempty"`
|
MaintenanceWindows []MaintenanceWindow `json:"maintenanceWindows,omitempty"`
|
||||||
Clone *CloneDescription `json:"clone,omitempty"`
|
Clone *CloneDescription `json:"clone,omitempty"`
|
||||||
ClusterName string `json:"-"`
|
ClusterName string `json:"-"`
|
||||||
|
|
|
||||||
|
|
@ -641,6 +641,16 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) {
|
||||||
(*out)[key] = outVal
|
(*out)[key] = outVal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if in.UsersWithSecretRotation != nil {
|
||||||
|
in, out := &in.UsersWithSecretRotation, &out.UsersWithSecretRotation
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
if in.UsersWithInPlaceSecretRotation != nil {
|
||||||
|
in, out := &in.UsersWithInPlaceSecretRotation, &out.UsersWithInPlaceSecretRotation
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
if in.MaintenanceWindows != nil {
|
if in.MaintenanceWindows != nil {
|
||||||
in, out := &in.MaintenanceWindows, &out.MaintenanceWindows
|
in, out := &in.MaintenanceWindows, &out.MaintenanceWindows
|
||||||
*out = make([]MaintenanceWindow, len(*in))
|
*out = make([]MaintenanceWindow, len(*in))
|
||||||
|
|
|
||||||
|
|
@ -1001,6 +1001,7 @@ func (c *Cluster) initSystemUsers() {
|
||||||
Origin: spec.RoleOriginSystem,
|
Origin: spec.RoleOriginSystem,
|
||||||
Name: c.OpConfig.ReplicationUsername,
|
Name: c.OpConfig.ReplicationUsername,
|
||||||
Namespace: c.Namespace,
|
Namespace: c.Namespace,
|
||||||
|
Flags: []string{constants.RoleFlagLogin},
|
||||||
Password: util.RandomPassword(constants.PasswordLength),
|
Password: util.RandomPassword(constants.PasswordLength),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,34 +14,11 @@ import (
|
||||||
"github.com/zalando/postgres-operator/pkg/spec"
|
"github.com/zalando/postgres-operator/pkg/spec"
|
||||||
"github.com/zalando/postgres-operator/pkg/util/constants"
|
"github.com/zalando/postgres-operator/pkg/util/constants"
|
||||||
"github.com/zalando/postgres-operator/pkg/util/retryutil"
|
"github.com/zalando/postgres-operator/pkg/util/retryutil"
|
||||||
|
"github.com/zalando/postgres-operator/pkg/util/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
getUserSQL = `WITH dbowners AS (
|
getUserSQL = `SELECT a.rolname, COALESCE(a.rolpassword, ''), a.rolsuper, a.rolinherit,
|
||||||
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,
|
a.rolcreaterole, a.rolcreatedb, a.rolcanlogin, s.setconfig,
|
||||||
ARRAY(SELECT b.rolname
|
ARRAY(SELECT b.rolname
|
||||||
FROM pg_catalog.pg_auth_members m
|
FROM pg_catalog.pg_auth_members m
|
||||||
|
|
@ -49,7 +26,13 @@ const (
|
||||||
WHERE m.member = a.oid) as memberof
|
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)
|
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)
|
WHERE a.rolname = ANY($1)
|
||||||
ORDER BY 1;`*/
|
ORDER BY 1;`
|
||||||
|
|
||||||
|
getUsersForRetention = `SELECT r.rolname, right(r.rolname, 6) AS roldatesuffix
|
||||||
|
FROM pg_roles r
|
||||||
|
JOIN unnest($1::text[]) AS u(name) ON r.rolname LIKE u.name || '%'
|
||||||
|
AND right(r.rolname, 6) ~ '^[0-9\.]+$'
|
||||||
|
ORDER BY 1;`
|
||||||
|
|
||||||
getDatabasesSQL = `SELECT datname, pg_get_userbyid(datdba) AS owner FROM pg_database;`
|
getDatabasesSQL = `SELECT datname, pg_get_userbyid(datdba) AS owner FROM pg_database;`
|
||||||
getSchemasSQL = `SELECT n.nspname AS dbschema FROM pg_catalog.pg_namespace n
|
getSchemasSQL = `SELECT n.nspname AS dbschema FROM pg_catalog.pg_namespace n
|
||||||
|
|
@ -222,10 +205,10 @@ func (c *Cluster) readPgUsersFromDatabase(userNames []string) (users spec.PgUser
|
||||||
rolname, rolpassword string
|
rolname, rolpassword string
|
||||||
rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin bool
|
rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin bool
|
||||||
roloptions, memberof []string
|
roloptions, memberof []string
|
||||||
rolowner, roldeleted bool
|
roldeleted bool
|
||||||
)
|
)
|
||||||
err := rows.Scan(&rolname, &rolpassword, &rolsuper, &rolinherit,
|
err := rows.Scan(&rolname, &rolpassword, &rolsuper, &rolinherit,
|
||||||
&rolcreaterole, &rolcreatedb, &rolcanlogin, pq.Array(&roloptions), pq.Array(&memberof), &rolowner)
|
&rolcreaterole, &rolcreatedb, &rolcanlogin, pq.Array(&roloptions), pq.Array(&memberof))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error when processing user rows: %v", err)
|
return nil, fmt.Errorf("error when processing user rows: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -245,12 +228,70 @@ func (c *Cluster) readPgUsersFromDatabase(userNames []string) (users spec.PgUser
|
||||||
roldeleted = true
|
roldeleted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
users[rolname] = spec.PgUser{Name: rolname, Password: rolpassword, Flags: flags, MemberOf: memberof, Parameters: parameters, IsDbOwner: rolowner, Deleted: roldeleted}
|
users[rolname] = spec.PgUser{Name: rolname, Password: rolpassword, Flags: flags, MemberOf: memberof, Parameters: parameters, Deleted: roldeleted}
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findUsersFromRotation(rotatedUsers []string, db *sql.DB) (map[string]string, error) {
|
||||||
|
extraUsers := make(map[string]string, 0)
|
||||||
|
rows, err := db.Query(getUsersForRetention, pq.Array(rotatedUsers))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("query failed: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err2 := rows.Close(); err2 != nil {
|
||||||
|
err = fmt.Errorf("error when closing query cursor: %v", err2)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
rolname, roldatesuffix string
|
||||||
|
)
|
||||||
|
err := rows.Scan(&rolname, &roldatesuffix)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error when processing rows of deprecated users: %v", err)
|
||||||
|
}
|
||||||
|
extraUsers[rolname] = roldatesuffix
|
||||||
|
}
|
||||||
|
|
||||||
|
return extraUsers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cluster) cleanupRotatedUsers(rotatedUsers []string, db *sql.DB) error {
|
||||||
|
c.setProcessName("checking for rotated users to remove from the database due to configured retention")
|
||||||
|
extraUsers, err := findUsersFromRotation(rotatedUsers, db)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error when querying for deprecated users from password rotation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for rotatedUser, dateSuffix := range extraUsers {
|
||||||
|
// make sure user retention policy aligns with rotation interval
|
||||||
|
retenionDays := c.OpConfig.PasswordRotationUserRetention
|
||||||
|
if retenionDays < 2*c.OpConfig.PasswordRotationInterval {
|
||||||
|
retenionDays = 2 * c.OpConfig.PasswordRotationInterval
|
||||||
|
c.logger.Warnf("user retention days too few compared to rotation interval %d - setting it to %d", c.OpConfig.PasswordRotationInterval, retenionDays)
|
||||||
|
}
|
||||||
|
retentionDate := time.Now().AddDate(0, 0, int(retenionDays)*-1)
|
||||||
|
userCreationDate, err := time.Parse("060102", dateSuffix)
|
||||||
|
if err != nil {
|
||||||
|
c.logger.Errorf("could not parse creation date suffix of user %q: %v", rotatedUser, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if retentionDate.After(userCreationDate) {
|
||||||
|
c.logger.Infof("dropping user %q due to configured days in password_rotation_user_retention", rotatedUser)
|
||||||
|
if err = users.DropPgUser(rotatedUser, db); err != nil {
|
||||||
|
c.logger.Errorf("could not drop role %q: %v", rotatedUser, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// getDatabases returns the map of current databases with owners
|
// getDatabases returns the map of current databases with owners
|
||||||
// The caller is responsible for opening and closing the database connection
|
// The caller is responsible for opening and closing the database connection
|
||||||
func (c *Cluster) getDatabases() (dbs map[string]string, err error) {
|
func (c *Cluster) getDatabases() (dbs map[string]string, err error) {
|
||||||
|
|
|
||||||
|
|
@ -627,6 +627,7 @@ func (c *Cluster) syncSecrets() error {
|
||||||
c.setProcessName("syncing secrets")
|
c.setProcessName("syncing secrets")
|
||||||
secrets := c.generateUserSecrets()
|
secrets := c.generateUserSecrets()
|
||||||
rotationUsers := make(spec.PgUserMap)
|
rotationUsers := make(spec.PgUserMap)
|
||||||
|
retentionUsers := make([]string, 0)
|
||||||
|
|
||||||
for secretUsername, secretSpec := range secrets {
|
for secretUsername, secretSpec := range secrets {
|
||||||
if secret, err = c.KubeClient.Secrets(secretSpec.Namespace).Create(context.TODO(), secretSpec, metav1.CreateOptions{}); err == nil {
|
if secret, err = c.KubeClient.Secrets(secretSpec.Namespace).Create(context.TODO(), secretSpec, metav1.CreateOptions{}); err == nil {
|
||||||
|
|
@ -672,7 +673,8 @@ func (c *Cluster) syncSecrets() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if password rotation is enabled update password and username if rotation interval has been passed
|
// if password rotation is enabled update password and username if rotation interval has been passed
|
||||||
if c.OpConfig.EnablePasswordRotation && pwdUser.Origin != spec.RoleOriginInfrastructure && !pwdUser.IsDbOwner { // || c.Spec.InPlacePasswordRotation[secretUsername] {
|
if (c.OpConfig.EnablePasswordRotation && pwdUser.Origin != spec.RoleOriginInfrastructure && !pwdUser.IsDbOwner) ||
|
||||||
|
util.SliceContains(c.Spec.UsersWithSecretRotation, secretUsername) || util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername) {
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
|
|
||||||
// initialize password rotation setting first rotation date
|
// initialize password rotation setting first rotation date
|
||||||
|
|
@ -688,13 +690,14 @@ func (c *Cluster) syncSecrets() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentTime.After(nextRotationDate) {
|
if currentTime.After(nextRotationDate) {
|
||||||
//if !c.Spec.InPlacePasswordRotation[secretUsername] {
|
if !util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername) {
|
||||||
newRotationUsername := pwdUser.Name + "_" + currentTime.Format("060102")
|
retentionUsers = append(retentionUsers, pwdUser.Name)
|
||||||
pwdUser.MemberOf = []string{pwdUser.Name}
|
newRotationUsername := pwdUser.Name + currentTime.Format("060102")
|
||||||
pwdUser.Name = newRotationUsername
|
pwdUser.MemberOf = []string{pwdUser.Name}
|
||||||
rotationUsers[newRotationUsername] = pwdUser
|
pwdUser.Name = newRotationUsername
|
||||||
secret.Data["username"] = []byte(newRotationUsername)
|
rotationUsers[newRotationUsername] = pwdUser
|
||||||
//}
|
secret.Data["username"] = []byte(newRotationUsername)
|
||||||
|
}
|
||||||
secret.Data["password"] = []byte(util.RandomPassword(constants.PasswordLength))
|
secret.Data["password"] = []byte(util.RandomPassword(constants.PasswordLength))
|
||||||
|
|
||||||
_, nextRotationDateStr = c.getNextRotationDate(nextRotationDate)
|
_, nextRotationDateStr = c.getNextRotationDate(nextRotationDate)
|
||||||
|
|
@ -722,14 +725,13 @@ func (c *Cluster) syncSecrets() error {
|
||||||
}
|
}
|
||||||
pgSyncRequests := c.userSyncStrategy.ProduceSyncRequests(spec.PgUserMap{}, rotationUsers)
|
pgSyncRequests := c.userSyncStrategy.ProduceSyncRequests(spec.PgUserMap{}, rotationUsers)
|
||||||
if err = c.userSyncStrategy.ExecuteSyncRequests(pgSyncRequests, c.pgDb); err != nil {
|
if err = c.userSyncStrategy.ExecuteSyncRequests(pgSyncRequests, c.pgDb); err != nil {
|
||||||
return fmt.Errorf("error executing sync statements: %v", err)
|
return fmt.Errorf("error creating database roles for password rotation: %v", err)
|
||||||
}
|
}
|
||||||
if err2 := c.closeDbConn(); err2 != nil {
|
if err = c.cleanupRotatedUsers(retentionUsers, c.pgDb); err != nil {
|
||||||
if err == nil {
|
return fmt.Errorf("error creating database roles for password rotation: %v", err)
|
||||||
return fmt.Errorf("could not close database connection: %v", err2)
|
}
|
||||||
} else {
|
if err := c.closeDbConn(); err != nil {
|
||||||
return fmt.Errorf("could not close database connection: %v (prior error: %v)", err2, err)
|
c.logger.Errorf("could not close database connection during secret rotation: %v", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ type Auth struct {
|
||||||
ReplicationUsername string `name:"replication_username" default:"standby"`
|
ReplicationUsername string `name:"replication_username" default:"standby"`
|
||||||
EnablePasswordRotation bool `name:"enable_password_rotation" default:"false"`
|
EnablePasswordRotation bool `name:"enable_password_rotation" default:"false"`
|
||||||
PasswordRotationInterval uint32 `name:"password_rotation_interval" default:"90"`
|
PasswordRotationInterval uint32 `name:"password_rotation_interval" default:"90"`
|
||||||
|
PasswordRotationUserRetention uint32 `name:"password_rotation_user_retention" default:"180"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scalyr holds the configuration for the Scalyr Agent sidecar for log shipping:
|
// Scalyr holds the configuration for the Scalyr Agent sidecar for log shipping:
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ const (
|
||||||
alterUserRenameSQL = `ALTER ROLE "%s" RENAME TO "%s%s"`
|
alterUserRenameSQL = `ALTER ROLE "%s" RENAME TO "%s%s"`
|
||||||
alterRoleResetAllSQL = `ALTER ROLE "%s" RESET ALL`
|
alterRoleResetAllSQL = `ALTER ROLE "%s" RESET ALL`
|
||||||
alterRoleSetSQL = `ALTER ROLE "%s" SET %s TO %s`
|
alterRoleSetSQL = `ALTER ROLE "%s" SET %s TO %s`
|
||||||
|
dropUserSQL = `SET LOCAL synchronous_commit = 'local'; DROP ROLE "%s";`
|
||||||
grantToUserSQL = `GRANT %s TO "%s"`
|
grantToUserSQL = `GRANT %s TO "%s"`
|
||||||
doBlockStmt = `SET LOCAL synchronous_commit = 'local'; DO $$ BEGIN %s; END;$$;`
|
doBlockStmt = `SET LOCAL synchronous_commit = 'local'; DO $$ BEGIN %s; END;$$;`
|
||||||
passwordTemplate = "ENCRYPTED PASSWORD '%s'"
|
passwordTemplate = "ENCRYPTED PASSWORD '%s'"
|
||||||
|
|
@ -288,3 +289,13 @@ func quoteParameterValue(name, val string) string {
|
||||||
}
|
}
|
||||||
return fmt.Sprintf(`'%s'`, strings.Trim(val, " "))
|
return fmt.Sprintf(`'%s'`, strings.Trim(val, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DropPgUser to remove user created by the operator e.g. for password rotation
|
||||||
|
func DropPgUser(user string, db *sql.DB) error {
|
||||||
|
query := fmt.Sprintf(dropUserSQL, user)
|
||||||
|
if _, err := db.Exec(query); err != nil { // TODO: Try several times
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue