Skip to content

Commit 886cb86

Browse files
authored
allow users to opt out from globally enabled secret rotation (#2528)
* allow users to opt out from globally enabled secret rotation * cover new option also in e2e test * change ignore test to existing user
1 parent 29ea863 commit 886cb86

File tree

11 files changed

+81
-10
lines changed

11 files changed

+81
-10
lines changed

charts/postgres-operator/crds/postgresqls.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,11 @@ spec:
612612
- SUPERUSER
613613
- nosuperuser
614614
- NOSUPERUSER
615+
usersIgnoringSecretRotation:
616+
type: array
617+
nullable: true
618+
items:
619+
type: string
615620
usersWithInPlaceSecretRotation:
616621
type: array
617622
nullable: true

docs/administrator.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,23 @@ This would be the recommended option to enable rotation in secrets of database
355355
owners, but only if they are not used as application users for regular read
356356
and write operations.
357357
358+
### Ignore rotation for certain users
359+
360+
If you wish to globally enable password rotation but need certain users to
361+
opt out from it there are two ways. First, you can remove the user from the
362+
manifest's `users` section. The corresponding secret to this user will no
363+
longer be synced by the operator then.
364+
365+
Secondly, if you want the operator to continue syncing the secret (e.g. to
366+
recreate if it got accidentally removed) but cannot allow it being rotated,
367+
add the user to the following list in your manifest:
368+
369+
```
370+
spec:
371+
usersIgnoringSecretRotation:
372+
- bar_user
373+
```
374+
358375
### Turning off password rotation
359376
360377
When password rotation is turned off again the operator will check if the

docs/reference/cluster_manifest.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ These parameters are grouped directly under the `spec` key in the manifest.
142142
database, like a flyway user running a migration on Pod start. See more
143143
details in the [administrator docs](https://github.com/zalando/postgres-operator/blob/master/docs/administrator.md#password-replacement-without-extra-users).
144144

145+
* **usersIgnoringSecretRotation**
146+
if you have secret rotation enabled globally you can define a list of
147+
of users that should opt out from it, for example if you store credentials
148+
outside of K8s, too, and corresponding deployments cannot dynamically
149+
reference secrets. Note, you can also opt out from the rotation by removing
150+
users from the manifest's `users` section. The operator will not drop them
151+
from the database. Optional.
152+
145153
* **databases**
146154
a map of database names to database owners for the databases that should be
147155
created by the operator. The owner users should already exist on the cluster

e2e/tests/test_e2e.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,15 +1578,18 @@ def test_password_rotation(self):
15781578
today = date.today()
15791579

15801580
# enable password rotation for owner of foo database
1581-
pg_patch_inplace_rotation_for_owner = {
1581+
pg_patch_rotation_single_users = {
15821582
"spec": {
1583+
"usersIgnoringSecretRotation": [
1584+
"test.db_user"
1585+
],
15831586
"usersWithInPlaceSecretRotation": [
15841587
"zalando"
15851588
]
15861589
}
15871590
}
15881591
k8s.api.custom_objects_api.patch_namespaced_custom_object(
1589-
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_inplace_rotation_for_owner)
1592+
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_rotation_single_users)
15901593
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
15911594

15921595
# check if next rotation date was set in secret
@@ -1675,6 +1678,13 @@ def test_password_rotation(self):
16751678
self.eventuallyEqual(lambda: len(self.query_database_with_user(leader.metadata.name, "postgres", "SELECT 1", "foo_user")), 1,
16761679
"Could not connect to the database with rotation user {}".format(rotation_user), 10, 5)
16771680

1681+
# check if rotation has been ignored for user from test_cross_namespace_secrets test
1682+
db_user_secret = k8s.get_secret(username="test.db_user", namespace="test")
1683+
secret_username = str(base64.b64decode(db_user_secret.data["username"]), 'utf-8')
1684+
1685+
self.assertEqual("test.db_user", secret_username,
1686+
"Unexpected username in secret of test.db_user: expected {}, got {}".format("test.db_user", secret_username))
1687+
16781688
# disable password rotation for all other users (foo_user)
16791689
# and pick smaller intervals to see if the third fake rotation user is dropped
16801690
enable_password_rotation = {

manifests/complete-postgres-manifest.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ spec:
1919
- createdb
2020
foo_user: []
2121
# flyway: []
22+
# usersIgnoringSecretRotation:
23+
# - bar_user
2224
# usersWithSecretRotation:
2325
# - foo_user
2426
# usersWithInPlaceSecretRotation:

manifests/postgresql.crd.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,11 @@ spec:
610610
- SUPERUSER
611611
- nosuperuser
612612
- NOSUPERUSER
613+
usersIgnoringSecretRotation:
614+
type: array
615+
nullable: true
616+
items:
617+
type: string
613618
usersWithInPlaceSecretRotation:
614619
type: array
615620
nullable: true

pkg/apis/acid.zalan.do/v1/crds.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,15 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
996996
},
997997
},
998998
},
999+
"usersIgnoringSecretRotation": {
1000+
Type: "array",
1001+
Nullable: true,
1002+
Items: &apiextv1.JSONSchemaPropsOrArray{
1003+
Schema: &apiextv1.JSONSchemaProps{
1004+
Type: "string",
1005+
},
1006+
},
1007+
},
9991008
"usersWithInPlaceSecretRotation": {
10001009
Type: "array",
10011010
Nullable: true,

pkg/apis/acid.zalan.do/v1/postgresql_type.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type PostgresSpec struct {
5959
AllowedSourceRanges []string `json:"allowedSourceRanges"`
6060

6161
Users map[string]UserFlags `json:"users,omitempty"`
62+
UsersIgnoringSecretRotation []string `json:"usersIgnoringSecretRotation,omitempty"`
6263
UsersWithSecretRotation []string `json:"usersWithSecretRotation,omitempty"`
6364
UsersWithInPlaceSecretRotation []string `json:"usersWithInPlaceSecretRotation,omitempty"`
6465

pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/cluster/sync.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/zalando/postgres-operator/pkg/util"
1616
"github.com/zalando/postgres-operator/pkg/util/constants"
1717
"github.com/zalando/postgres-operator/pkg/util/k8sutil"
18+
"golang.org/x/exp/slices"
1819
batchv1 "k8s.io/api/batch/v1"
1920
v1 "k8s.io/api/core/v1"
2021
policyv1 "k8s.io/api/policy/v1"
@@ -689,7 +690,7 @@ func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration(pod *v1.Pod, effectiv
689690
effectiveValue := effectivePgParameters[desiredOption]
690691
if isBootstrapOnlyParameter(desiredOption) && (effectiveValue != desiredValue) {
691692
parametersToSet[desiredOption] = desiredValue
692-
if util.SliceContains(requirePrimaryRestartWhenDecreased, desiredOption) {
693+
if slices.Contains(requirePrimaryRestartWhenDecreased, desiredOption) {
693694
effectiveValueNum, errConv := strconv.Atoi(effectiveValue)
694695
desiredValueNum, errConv2 := strconv.Atoi(desiredValue)
695696
if errConv != nil || errConv2 != nil {
@@ -705,7 +706,7 @@ func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration(pod *v1.Pod, effectiv
705706
}
706707

707708
// check if there exist only config updates that require a restart of the primary
708-
if len(restartPrimary) > 0 && !util.SliceContains(restartPrimary, false) && len(configToSet) == 0 {
709+
if len(restartPrimary) > 0 && !slices.Contains(restartPrimary, false) && len(configToSet) == 0 {
709710
requiresMasterRestart = true
710711
}
711712

@@ -873,14 +874,17 @@ func (c *Cluster) updateSecret(
873874
// if password rotation is enabled update password and username if rotation interval has been passed
874875
// rotation can be enabled globally or via the manifest (excluding the Postgres superuser)
875876
rotationEnabledInManifest := secretUsername != constants.SuperuserKeyName &&
876-
(util.SliceContains(c.Spec.UsersWithSecretRotation, secretUsername) ||
877-
util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername))
877+
(slices.Contains(c.Spec.UsersWithSecretRotation, secretUsername) ||
878+
slices.Contains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername))
878879

879880
// globally enabled rotation is only allowed for manifest and bootstrapped roles
880881
allowedRoleTypes := []spec.RoleOrigin{spec.RoleOriginManifest, spec.RoleOriginBootstrap}
881-
rotationAllowed := !pwdUser.IsDbOwner && util.SliceContains(allowedRoleTypes, pwdUser.Origin) && c.Spec.StandbyCluster == nil
882+
rotationAllowed := !pwdUser.IsDbOwner && slices.Contains(allowedRoleTypes, pwdUser.Origin) && c.Spec.StandbyCluster == nil
882883

883-
if (c.OpConfig.EnablePasswordRotation && rotationAllowed) || rotationEnabledInManifest {
884+
// users can ignore any kind of rotation
885+
isIgnoringRotation := slices.Contains(c.Spec.UsersIgnoringSecretRotation, secretUsername)
886+
887+
if ((c.OpConfig.EnablePasswordRotation && rotationAllowed) || rotationEnabledInManifest) && !isIgnoringRotation {
884888
updateSecretMsg, err = c.rotatePasswordInSecret(secret, secretUsername, pwdUser.Origin, currentTime, retentionUsers)
885889
if err != nil {
886890
c.logger.Warnf("password rotation failed for user %s: %v", secretUsername, err)
@@ -961,7 +965,7 @@ func (c *Cluster) rotatePasswordInSecret(
961965
// update password and next rotation date if configured interval has passed
962966
if currentTime.After(nextRotationDate) {
963967
// create rotation user if role is not listed for in-place password update
964-
if !util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername) {
968+
if !slices.Contains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername) {
965969
rotationUsername := fmt.Sprintf("%s%s", secretUsername, currentTime.Format(constants.RotationUserDateFormat))
966970
secret.Data["username"] = []byte(rotationUsername)
967971
c.logger.Infof("updating username in secret %s and creating rotation user %s in the database", secretName, rotationUsername)

pkg/cluster/sync_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"context"
1212

13+
"golang.org/x/exp/slices"
1314
v1 "k8s.io/api/core/v1"
1415
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1516
"k8s.io/apimachinery/pkg/types"
@@ -634,7 +635,8 @@ func TestUpdateSecret(t *testing.T) {
634635
},
635636
Spec: acidv1.PostgresSpec{
636637
Databases: map[string]string{dbname: dbowner},
637-
Users: map[string]acidv1.UserFlags{"foo": {}, dbowner: {}},
638+
Users: map[string]acidv1.UserFlags{"foo": {}, "bar": {}, dbowner: {}},
639+
UsersIgnoringSecretRotation: []string{"bar"},
638640
UsersWithInPlaceSecretRotation: []string{dbowner},
639641
Streams: []acidv1.Stream{
640642
{
@@ -712,6 +714,9 @@ func TestUpdateSecret(t *testing.T) {
712714
if pgUser.Origin != spec.RoleOriginManifest {
713715
continue
714716
}
717+
if slices.Contains(pg.Spec.UsersIgnoringSecretRotation, username) {
718+
continue
719+
}
715720
t.Errorf("%s: password unchanged in updated secret for %s", testName, username)
716721
}
717722

0 commit comments

Comments
 (0)