Skip to content

Commit 4786f53

Browse files
authored
Fix password rotation (zalando#2043)
* fix password rotation * test connection with rotation user in e2e test + minor changes
1 parent e1de445 commit 4786f53

File tree

3 files changed

+50
-41
lines changed

3 files changed

+50
-41
lines changed

e2e/tests/test_e2e.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1442,6 +1442,10 @@ def test_password_rotation(self):
14421442
self.eventuallyEqual(lambda: len(self.query_database(leader.metadata.name, "postgres", user_query)), 3,
14431443
"Found incorrect number of rotation users", 10, 5)
14441444

1445+
# test that rotation_user can connect to the database
1446+
self.eventuallyEqual(lambda: len(self.query_database_with_user(leader.metadata.name, "postgres", "SELECT 1", "foo_user")), 1,
1447+
"Could not connect to the database with rotation user {}".format(rotation_user), 10, 5)
1448+
14451449
# disable password rotation for all other users (foo_user)
14461450
# and pick smaller intervals to see if the third fake rotation user is dropped
14471451
enable_password_rotation = {
@@ -1998,5 +2002,28 @@ def query_database(self, pod_name, db_name, query):
19982002

19992003
return result_set
20002004

2005+
def query_database_with_user(self, pod_name, db_name, query, user_name):
2006+
'''
2007+
Query database and return result as a list
2008+
'''
2009+
k8s = self.k8s
2010+
result_set = []
2011+
exec_query = r"PGPASSWORD={} psql -h localhost -U {} -tAq -c \"{}\" -d {}"
2012+
2013+
try:
2014+
user_secret = k8s.get_secret(user_name)
2015+
secret_user = str(base64.b64decode(user_secret.data["username"]), 'utf-8')
2016+
secret_pw = str(base64.b64decode(user_secret.data["password"]), 'utf-8')
2017+
q = exec_query.format(secret_pw, secret_user, query, db_name)
2018+
q = "su postgres -c \"{}\"".format(q)
2019+
result = k8s.exec_with_kubectl(pod_name, q)
2020+
result_set = clean_list(result.stdout.split(b'\n'))
2021+
except Exception as ex:
2022+
print('Error on query execution: {}'.format(ex))
2023+
print('Stdout: {}'.format(result.stdout))
2024+
print('Stderr: {}'.format(result.stderr))
2025+
2026+
return result_set
2027+
20012028
if __name__ == '__main__':
2002-
unittest.main()
2029+
unittest.main()

pkg/cluster/sync.go

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,6 @@ func (c *Cluster) syncSecrets() error {
638638
c.logger.Info("syncing secrets")
639639
c.setProcessName("syncing secrets")
640640
generatedSecrets := c.generateUserSecrets()
641-
rotationUsers := make(spec.PgUserMap)
642641
retentionUsers := make([]string, 0)
643642
currentTime := time.Now()
644643

@@ -650,29 +649,14 @@ func (c *Cluster) syncSecrets() error {
650649
continue
651650
}
652651
if k8sutil.ResourceAlreadyExists(err) {
653-
if err = c.updateSecret(secretUsername, generatedSecret, &rotationUsers, &retentionUsers, currentTime); err != nil {
652+
if err = c.updateSecret(secretUsername, generatedSecret, &retentionUsers, currentTime); err != nil {
654653
c.logger.Warningf("syncing secret %s failed: %v", util.NameFromMeta(secret.ObjectMeta), err)
655654
}
656655
} else {
657656
return fmt.Errorf("could not create secret for user %s: in namespace %s: %v", secretUsername, generatedSecret.Namespace, err)
658657
}
659658
}
660659

661-
// add new user with date suffix and use it in the secret of the original user
662-
if len(rotationUsers) > 0 {
663-
err := c.initDbConn()
664-
if err != nil {
665-
return fmt.Errorf("could not init db connection: %v", err)
666-
}
667-
pgSyncRequests := c.userSyncStrategy.ProduceSyncRequests(spec.PgUserMap{}, rotationUsers)
668-
if err = c.userSyncStrategy.ExecuteSyncRequests(pgSyncRequests, c.pgDb); err != nil {
669-
return fmt.Errorf("error creating database roles for password rotation: %v", err)
670-
}
671-
if err := c.closeDbConn(); err != nil {
672-
c.logger.Errorf("could not close database connection after creating users for password rotation: %v", err)
673-
}
674-
}
675-
676660
// remove rotation users that exceed the retention interval
677661
if len(retentionUsers) > 0 {
678662
err := c.initDbConn()
@@ -698,7 +682,6 @@ func (c *Cluster) getNextRotationDate(currentDate time.Time) (time.Time, string)
698682
func (c *Cluster) updateSecret(
699683
secretUsername string,
700684
generatedSecret *v1.Secret,
701-
rotationUsers *spec.PgUserMap,
702685
retentionUsers *[]string,
703686
currentTime time.Time) error {
704687
var (
@@ -757,7 +740,7 @@ func (c *Cluster) updateSecret(
757740
rotationAllowed := !pwdUser.IsDbOwner && util.SliceContains(allowedRoleTypes, pwdUser.Origin)
758741

759742
if (c.OpConfig.EnablePasswordRotation && rotationAllowed) || rotationEnabledInManifest {
760-
updateSecretMsg, err = c.rotatePasswordInSecret(secret, pwdUser, secretUsername, currentTime, rotationUsers, retentionUsers)
743+
updateSecretMsg, err = c.rotatePasswordInSecret(secret, secretUsername, pwdUser.Origin, currentTime, retentionUsers)
761744
if err != nil {
762745
c.logger.Warnf("password rotation failed for user %s: %v", secretUsername, err)
763746
}
@@ -782,8 +765,13 @@ func (c *Cluster) updateSecret(
782765
updateSecret = true
783766
updateSecretMsg = fmt.Sprintf("updating the secret %s from the infrastructure roles", secretName)
784767
} else {
785-
// for non-infrastructure role - update the role with the password from the secret
768+
// for non-infrastructure role - update the role with username and password from secret
769+
pwdUser.Name = string(secret.Data["username"])
786770
pwdUser.Password = string(secret.Data["password"])
771+
// update membership if we deal with a rotation user
772+
if secretUsername != pwdUser.Name {
773+
pwdUser.MemberOf = []string{secretUsername}
774+
}
787775
userMap[userKey] = pwdUser
788776
}
789777

@@ -800,10 +788,9 @@ func (c *Cluster) updateSecret(
800788

801789
func (c *Cluster) rotatePasswordInSecret(
802790
secret *v1.Secret,
803-
secretPgUser spec.PgUser,
804791
secretUsername string,
792+
roleOrigin spec.RoleOrigin,
805793
currentTime time.Time,
806-
rotationUsers *spec.PgUserMap,
807794
retentionUsers *[]string) (string, error) {
808795
var (
809796
err error
@@ -833,18 +820,14 @@ func (c *Cluster) rotatePasswordInSecret(
833820
if currentTime.After(nextRotationDate) {
834821
// create rotation user if role is not listed for in-place password update
835822
if !util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername) {
836-
rotationUser := secretPgUser
837-
newRotationUsername := fmt.Sprintf("%s%s", secretUsername, currentTime.Format("060102"))
838-
rotationUser.Name = newRotationUsername
839-
rotationUser.MemberOf = []string{secretUsername}
840-
(*rotationUsers)[newRotationUsername] = rotationUser
841-
secret.Data["username"] = []byte(newRotationUsername)
842-
823+
rotationUsername := fmt.Sprintf("%s%s", secretUsername, currentTime.Format("060102"))
824+
secret.Data["username"] = []byte(rotationUsername)
825+
c.logger.Infof("updating username in secret %s and creating rotation user %s in the database", secretName, rotationUsername)
843826
// whenever there is a rotation, check if old rotation users can be deleted
844827
*retentionUsers = append(*retentionUsers, secretUsername)
845828
} else {
846829
// when passwords of system users are rotated in place, pods have to be replaced
847-
if secretPgUser.Origin == spec.RoleOriginSystem {
830+
if roleOrigin == spec.RoleOriginSystem {
848831
pods, err := c.listPods()
849832
if err != nil {
850833
return "", fmt.Errorf("could not list pods of the statefulset: %v", err)
@@ -858,7 +841,7 @@ func (c *Cluster) rotatePasswordInSecret(
858841
}
859842

860843
// when password of connection pooler is rotated in place, pooler pods have to be replaced
861-
if secretPgUser.Origin == spec.RoleOriginConnectionPooler {
844+
if roleOrigin == spec.RoleOriginConnectionPooler {
862845
listOptions := metav1.ListOptions{
863846
LabelSelector: c.poolerLabelsSet(true).String(),
864847
}
@@ -875,8 +858,8 @@ func (c *Cluster) rotatePasswordInSecret(
875858
}
876859

877860
// when password of stream user is rotated in place, it should trigger rolling update in FES deployment
878-
if secretPgUser.Origin == spec.RoleOriginStream {
879-
c.logger.Warnf("secret of stream user %s changed", constants.EventStreamSourceSlotPrefix+constants.UserRoleNameSuffix)
861+
if roleOrigin == spec.RoleOriginStream {
862+
c.logger.Warnf("password in secret of stream user %s changed", constants.EventStreamSourceSlotPrefix+constants.UserRoleNameSuffix)
880863
}
881864
}
882865
secret.Data["password"] = []byte(util.RandomPassword(constants.PasswordLength))

pkg/cluster/sync_test.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,6 @@ func TestUpdateSecret(t *testing.T) {
276276
dbname := "app"
277277
dbowner := "appowner"
278278
secretTemplate := config.StringTemplate("{username}.{cluster}.credentials")
279-
rotationUsers := make(spec.PgUserMap)
280279
retentionUsers := make([]string, 0)
281280

282281
// define manifest users and enable rotation for dbowner
@@ -339,8 +338,8 @@ func TestUpdateSecret(t *testing.T) {
339338
dayAfterTomorrow := time.Now().AddDate(0, 0, 2)
340339

341340
allUsers := make(map[string]spec.PgUser)
342-
for userName, pgUser := range cluster.pgUsers {
343-
allUsers[userName] = pgUser
341+
for _, pgUser := range cluster.pgUsers {
342+
allUsers[pgUser.Name] = pgUser
344343
}
345344
for _, systemUser := range cluster.systemUsers {
346345
allUsers[systemUser.Name] = systemUser
@@ -354,7 +353,7 @@ func TestUpdateSecret(t *testing.T) {
354353
secretPassword := string(secret.Data["password"])
355354

356355
// now update the secret setting a next rotation date (tomorrow + interval)
357-
cluster.updateSecret(username, secret, &rotationUsers, &retentionUsers, dayAfterTomorrow)
356+
cluster.updateSecret(username, secret, &retentionUsers, dayAfterTomorrow)
358357
updatedSecret, err := cluster.KubeClient.Secrets(namespace).Get(context.TODO(), secretName, metav1.GetOptions{})
359358
assert.NoError(t, err)
360359

@@ -386,9 +385,9 @@ func TestUpdateSecret(t *testing.T) {
386385
if secretUsername != rotatedUsername {
387386
t.Errorf("%s: updated secret does not contain correct username: expected %s, got %s", testName, rotatedUsername, secretUsername)
388387
}
389-
390-
if len(rotationUsers) != 1 && len(retentionUsers) != 1 {
391-
t.Errorf("%s: unexpected number of users to rotate - expected only %s, found %d", testName, username, len(rotationUsers))
388+
// whenever there's a rotation the retentionUsers list is extended or updated
389+
if len(retentionUsers) != 1 {
390+
t.Errorf("%s: unexpected number of users to drop - expected only %s, found %d", testName, username, len(retentionUsers))
392391
}
393392
}
394393
}

0 commit comments

Comments
 (0)