Skip to content

Commit 8b404fd

Browse files
authored
minor fixes to password rotation (zalando#1796)
* minor fixes to password rotation * rework unit test
1 parent 06c28da commit 8b404fd

File tree

5 files changed

+82
-47
lines changed

5 files changed

+82
-47
lines changed

docs/administrator.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -306,10 +306,10 @@ The interval of days can be set with `password_rotation_interval` (default
306306
are replaced in the K8s secret. They belong to a newly created user named after
307307
the original role plus rotation date in YYMMDD format. All priviliges are
308308
inherited meaning that migration scripts should still grant and revoke rights
309-
against the original role. The timestamp of the next rotation is written to the
310-
secret as well. Note, if the rotation interval is decreased it is reflected in
311-
the secrets only if the next rotation date is more days away than the new
312-
length of the interval.
309+
against the original role. The timestamp of the next rotation (in RFC 3339
310+
format, UTC timezone) is written to the secret as well. Note, if the rotation
311+
interval is decreased it is reflected in the secrets only if the next rotation
312+
date is more days away than the new length of the interval.
313313

314314
Pods still using the previous secret values which they keep in memory continue
315315
to connect to the database since the password of the corresponding user is not

e2e/tests/test_e2e.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1232,7 +1232,7 @@ def test_password_rotation(self):
12321232

12331233
# check if next rotation date was set in secret
12341234
secret_data = k8s.get_secret_data("zalando")
1235-
next_rotation_timestamp = datetime.fromisoformat(str(base64.b64decode(secret_data["nextRotation"]), 'utf-8'))
1235+
next_rotation_timestamp = datetime.strptime(str(base64.b64decode(secret_data["nextRotation"]), 'utf-8'), "%Y-%m-%dT%H:%M:%SZ")
12361236
today90days = today+timedelta(days=90)
12371237
self.assertEqual(today90days, next_rotation_timestamp.date(),
12381238
"Unexpected rotation date in secret of zalando user: expected {}, got {}".format(today90days, next_rotation_timestamp.date()))
@@ -1247,7 +1247,7 @@ def test_password_rotation(self):
12471247
self.query_database(leader.metadata.name, "postgres", create_fake_rotation_user)
12481248

12491249
# patch foo_user secret with outdated rotation date
1250-
fake_rotation_date = today.isoformat() + ' 00:00:00'
1250+
fake_rotation_date = today.isoformat() + 'T00:00:00Z'
12511251
fake_rotation_date_encoded = base64.b64encode(fake_rotation_date.encode('utf-8'))
12521252
secret_fake_rotation = {
12531253
"data": {
@@ -1275,7 +1275,7 @@ def test_password_rotation(self):
12751275
# check if next rotation date and username have been replaced
12761276
secret_data = k8s.get_secret_data("foo_user")
12771277
secret_username = str(base64.b64decode(secret_data["username"]), 'utf-8')
1278-
next_rotation_timestamp = datetime.fromisoformat(str(base64.b64decode(secret_data["nextRotation"]), 'utf-8'))
1278+
next_rotation_timestamp = datetime.strptime(str(base64.b64decode(secret_data["nextRotation"]), 'utf-8'), "%Y-%m-%dT%H:%M:%SZ")
12791279
rotation_user = "foo_user"+today.strftime("%y%m%d")
12801280
today30days = today+timedelta(days=30)
12811281

manifests/complete-postgres-manifest.yaml

+6-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ spec:
1717
- superuser
1818
- createdb
1919
foo_user: []
20-
# usersWithSecretRotation: "foo_user"
21-
# usersWithInPlaceSecretRotation: "flyway,bar_owner_user"
20+
# flyway: []
21+
# usersWithSecretRotation:
22+
# - foo_user
23+
# usersWithInPlaceSecretRotation:
24+
# - flyway
25+
# - bar_owner_user
2226
enableMasterLoadBalancer: false
2327
enableReplicaLoadBalancer: false
2428
enableConnectionPooler: false # enable/disable connection pooler deployment

pkg/cluster/sync.go

+7-9
Original file line numberDiff line numberDiff line change
@@ -620,11 +620,6 @@ func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration(pod *v1.Pod, patroniC
620620
return requiresMasterRestart, nil
621621
}
622622

623-
func (c *Cluster) getNextRotationDate(currentDate time.Time) (time.Time, string) {
624-
nextRotationDate := currentDate.AddDate(0, 0, int(c.OpConfig.PasswordRotationInterval))
625-
return nextRotationDate, nextRotationDate.Format("2006-01-02 15:04:05")
626-
}
627-
628623
func (c *Cluster) syncSecrets() error {
629624

630625
c.logger.Info("syncing secrets")
@@ -682,6 +677,11 @@ func (c *Cluster) syncSecrets() error {
682677
return nil
683678
}
684679

680+
func (c *Cluster) getNextRotationDate(currentDate time.Time) (time.Time, string) {
681+
nextRotationDate := currentDate.AddDate(0, 0, int(c.OpConfig.PasswordRotationInterval))
682+
return nextRotationDate, nextRotationDate.Format(time.RFC3339)
683+
}
684+
685685
func (c *Cluster) updateSecret(
686686
secretUsername string,
687687
generatedSecret *v1.Secret,
@@ -727,7 +727,7 @@ func (c *Cluster) updateSecret(
727727

728728
// initialize password rotation setting first rotation date
729729
nextRotationDateStr = string(secret.Data["nextRotation"])
730-
if nextRotationDate, err = time.ParseInLocation("2006-01-02 15:04:05", nextRotationDateStr, time.Local); err != nil {
730+
if nextRotationDate, err = time.ParseInLocation(time.RFC3339, nextRotationDateStr, currentTime.UTC().Location()); err != nil {
731731
nextRotationDate, nextRotationDateStr = c.getNextRotationDate(currentTime)
732732
secret.Data["nextRotation"] = []byte(nextRotationDateStr)
733733
updateSecret = true
@@ -736,7 +736,7 @@ func (c *Cluster) updateSecret(
736736

737737
// check if next rotation can happen sooner
738738
// if rotation interval has been decreased
739-
currentRotationDate, _ := c.getNextRotationDate(currentTime)
739+
currentRotationDate, nextRotationDateStr := c.getNextRotationDate(currentTime)
740740
if nextRotationDate.After(currentRotationDate) {
741741
nextRotationDate = currentRotationDate
742742
}
@@ -756,8 +756,6 @@ func (c *Cluster) updateSecret(
756756
*retentionUsers = append(*retentionUsers, secretUsername)
757757
}
758758
secret.Data["password"] = []byte(util.RandomPassword(constants.PasswordLength))
759-
760-
_, nextRotationDateStr = c.getNextRotationDate(nextRotationDate)
761759
secret.Data["nextRotation"] = []byte(nextRotationDateStr)
762760

763761
updateSecret = true

pkg/cluster/sync_test.go

+62-29
Original file line numberDiff line numberDiff line change
@@ -270,13 +270,29 @@ func TestUpdateSecret(t *testing.T) {
270270

271271
clusterName := "acid-test-cluster"
272272
namespace := "default"
273-
username := "foo"
273+
dbname := "app"
274+
dbowner := "appowner"
274275
secretTemplate := config.StringTemplate("{username}.{cluster}.credentials")
275276
rotationUsers := make(spec.PgUserMap)
276277
retentionUsers := make([]string, 0)
277-
yesterday := time.Now().AddDate(0, 0, -1)
278278

279-
// new cluster with pvc storage resize mode and configured labels
279+
// define manifest users and enable rotation for dbowner
280+
pg := acidv1.Postgresql{
281+
ObjectMeta: metav1.ObjectMeta{
282+
Name: clusterName,
283+
Namespace: namespace,
284+
},
285+
Spec: acidv1.PostgresSpec{
286+
Databases: map[string]string{dbname: dbowner},
287+
Users: map[string]acidv1.UserFlags{"foo": {}, dbowner: {}},
288+
UsersWithInPlaceSecretRotation: []string{dbowner},
289+
Volume: acidv1.Volume{
290+
Size: "1Gi",
291+
},
292+
},
293+
}
294+
295+
// new cluster with enabled password rotation
280296
var cluster = New(
281297
Config{
282298
OpConfig: config.Config{
@@ -291,44 +307,61 @@ func TestUpdateSecret(t *testing.T) {
291307
ClusterNameLabel: "cluster-name",
292308
},
293309
},
294-
}, client, acidv1.Postgresql{}, logger, eventRecorder)
310+
}, client, pg, logger, eventRecorder)
295311

296312
cluster.Name = clusterName
297313
cluster.Namespace = namespace
298314
cluster.pgUsers = map[string]spec.PgUser{}
299-
cluster.Spec.Users = map[string]acidv1.UserFlags{username: {}}
300315
cluster.initRobotUsers()
301316

302-
// create a secret for user foo
317+
// create secrets
318+
cluster.syncSecrets()
319+
// initialize rotation with current time
303320
cluster.syncSecrets()
304321

305-
secret, err := cluster.KubeClient.Secrets(namespace).Get(context.TODO(), secretTemplate.Format("username", username, "cluster", clusterName), metav1.GetOptions{})
306-
assert.NoError(t, err)
307-
generatedSecret := cluster.Secrets[secret.UID]
322+
dayAfterTomorrow := time.Now().AddDate(0, 0, 2)
308323

309-
// now update the secret setting next rotation date (yesterday + interval)
310-
cluster.updateSecret(username, generatedSecret, &rotationUsers, &retentionUsers, yesterday)
311-
updatedSecret, err := cluster.KubeClient.Secrets(namespace).Get(context.TODO(), secretTemplate.Format("username", username, "cluster", clusterName), metav1.GetOptions{})
312-
assert.NoError(t, err)
324+
for username := range cluster.Spec.Users {
325+
pgUser := cluster.pgUsers[username]
313326

314-
nextRotation := string(updatedSecret.Data["nextRotation"])
315-
_, nextRotationDate := cluster.getNextRotationDate(yesterday)
316-
if nextRotation != nextRotationDate {
317-
t.Errorf("%s: updated secret does not contain correct rotation date: expected %s, got %s", testName, nextRotationDate, nextRotation)
318-
}
327+
// first, get the secret
328+
secret, err := cluster.KubeClient.Secrets(namespace).Get(context.TODO(), secretTemplate.Format("username", username, "cluster", clusterName), metav1.GetOptions{})
329+
assert.NoError(t, err)
330+
secretPassword := string(secret.Data["password"])
319331

320-
// update secret again but use current time to trigger rotation
321-
cluster.updateSecret(username, generatedSecret, &rotationUsers, &retentionUsers, time.Now())
322-
updatedSecret, err = cluster.KubeClient.Secrets(namespace).Get(context.TODO(), secretTemplate.Format("username", username, "cluster", clusterName), metav1.GetOptions{})
323-
assert.NoError(t, err)
332+
// now update the secret setting a next rotation date (tomorrow + interval)
333+
cluster.updateSecret(username, secret, &rotationUsers, &retentionUsers, dayAfterTomorrow)
334+
updatedSecret, err := cluster.KubeClient.Secrets(namespace).Get(context.TODO(), secretTemplate.Format("username", username, "cluster", clusterName), metav1.GetOptions{})
335+
assert.NoError(t, err)
324336

325-
if len(rotationUsers) != 1 && len(retentionUsers) != 1 {
326-
t.Errorf("%s: unexpected number of users to rotate - expected only foo, found %d", testName, len(rotationUsers))
327-
}
337+
// check that passwords are different
338+
rotatedPassword := string(updatedSecret.Data["password"])
339+
if secretPassword == rotatedPassword {
340+
t.Errorf("%s: password unchanged in updated secret for %s", testName, username)
341+
}
328342

329-
secretUsername := string(updatedSecret.Data["username"])
330-
rotatedUsername := username + time.Now().Format("060102")
331-
if secretUsername != rotatedUsername {
332-
t.Errorf("%s: updated secret does not contain correct username: expected %s, got %s", testName, rotatedUsername, secretUsername)
343+
// check that next rotation date is tomorrow + interval, not date in secret + interval
344+
nextRotation := string(updatedSecret.Data["nextRotation"])
345+
_, nextRotationDate := cluster.getNextRotationDate(dayAfterTomorrow)
346+
if nextRotation != nextRotationDate {
347+
t.Errorf("%s: updated secret of %s does not contain correct rotation date: expected %s, got %s", testName, username, nextRotationDate, nextRotation)
348+
}
349+
350+
// compare username, when it's dbowner they should be equal because of UsersWithInPlaceSecretRotation
351+
secretUsername := string(updatedSecret.Data["username"])
352+
if pgUser.IsDbOwner {
353+
if secretUsername != username {
354+
t.Errorf("%s: username differs in updated secret: expected %s, got %s", testName, username, secretUsername)
355+
}
356+
} else {
357+
rotatedUsername := username + dayAfterTomorrow.Format("060102")
358+
if secretUsername != rotatedUsername {
359+
t.Errorf("%s: updated secret does not contain correct username: expected %s, got %s", testName, rotatedUsername, secretUsername)
360+
}
361+
362+
if len(rotationUsers) != 1 && len(retentionUsers) != 1 {
363+
t.Errorf("%s: unexpected number of users to rotate - expected only %s, found %d", testName, username, len(rotationUsers))
364+
}
365+
}
333366
}
334367
}

0 commit comments

Comments
 (0)