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
This commit is contained in:
parent
29ea863faf
commit
886cb86797
|
|
@ -612,6 +612,11 @@ spec:
|
||||||
- SUPERUSER
|
- SUPERUSER
|
||||||
- nosuperuser
|
- nosuperuser
|
||||||
- NOSUPERUSER
|
- NOSUPERUSER
|
||||||
|
usersIgnoringSecretRotation:
|
||||||
|
type: array
|
||||||
|
nullable: true
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
usersWithInPlaceSecretRotation:
|
usersWithInPlaceSecretRotation:
|
||||||
type: array
|
type: array
|
||||||
nullable: true
|
nullable: true
|
||||||
|
|
|
||||||
|
|
@ -355,6 +355,23 @@ 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
|
owners, but only if they are not used as application users for regular read
|
||||||
and write operations.
|
and write operations.
|
||||||
|
|
||||||
|
### Ignore rotation for certain users
|
||||||
|
|
||||||
|
If you wish to globally enable password rotation but need certain users to
|
||||||
|
opt out from it there are two ways. First, you can remove the user from the
|
||||||
|
manifest's `users` section. The corresponding secret to this user will no
|
||||||
|
longer be synced by the operator then.
|
||||||
|
|
||||||
|
Secondly, if you want the operator to continue syncing the secret (e.g. to
|
||||||
|
recreate if it got accidentally removed) but cannot allow it being rotated,
|
||||||
|
add the user to the following list in your manifest:
|
||||||
|
|
||||||
|
```
|
||||||
|
spec:
|
||||||
|
usersIgnoringSecretRotation:
|
||||||
|
- bar_user
|
||||||
|
```
|
||||||
|
|
||||||
### Turning off password rotation
|
### Turning off password rotation
|
||||||
|
|
||||||
When password rotation is turned off again the operator will check if the
|
When password rotation is turned off again the operator will check if the
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,14 @@ These parameters are grouped directly under the `spec` key in the manifest.
|
||||||
database, like a flyway user running a migration on Pod start. See more
|
database, like a flyway user running a migration on Pod start. See more
|
||||||
details in the [administrator docs](https://github.com/zalando/postgres-operator/blob/master/docs/administrator.md#password-replacement-without-extra-users).
|
details in the [administrator docs](https://github.com/zalando/postgres-operator/blob/master/docs/administrator.md#password-replacement-without-extra-users).
|
||||||
|
|
||||||
|
* **usersIgnoringSecretRotation**
|
||||||
|
if you have secret rotation enabled globally you can define a list of
|
||||||
|
of users that should opt out from it, for example if you store credentials
|
||||||
|
outside of K8s, too, and corresponding deployments cannot dynamically
|
||||||
|
reference secrets. Note, you can also opt out from the rotation by removing
|
||||||
|
users from the manifest's `users` section. The operator will not drop them
|
||||||
|
from the database. Optional.
|
||||||
|
|
||||||
* **databases**
|
* **databases**
|
||||||
a map of database names to database owners for the databases that should be
|
a map of database names to database owners for the databases that should be
|
||||||
created by the operator. The owner users should already exist on the cluster
|
created by the operator. The owner users should already exist on the cluster
|
||||||
|
|
|
||||||
|
|
@ -1578,15 +1578,18 @@ class EndToEndTestCase(unittest.TestCase):
|
||||||
today = date.today()
|
today = date.today()
|
||||||
|
|
||||||
# enable password rotation for owner of foo database
|
# enable password rotation for owner of foo database
|
||||||
pg_patch_inplace_rotation_for_owner = {
|
pg_patch_rotation_single_users = {
|
||||||
"spec": {
|
"spec": {
|
||||||
|
"usersIgnoringSecretRotation": [
|
||||||
|
"test.db_user"
|
||||||
|
],
|
||||||
"usersWithInPlaceSecretRotation": [
|
"usersWithInPlaceSecretRotation": [
|
||||||
"zalando"
|
"zalando"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
k8s.api.custom_objects_api.patch_namespaced_custom_object(
|
||||||
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_inplace_rotation_for_owner)
|
"acid.zalan.do", "v1", "default", "postgresqls", "acid-minimal-cluster", pg_patch_rotation_single_users)
|
||||||
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
self.eventuallyEqual(lambda: k8s.get_operator_state(), {"0": "idle"}, "Operator does not get in sync")
|
||||||
|
|
||||||
# check if next rotation date was set in secret
|
# check if next rotation date was set in secret
|
||||||
|
|
@ -1675,6 +1678,13 @@ class EndToEndTestCase(unittest.TestCase):
|
||||||
self.eventuallyEqual(lambda: len(self.query_database_with_user(leader.metadata.name, "postgres", "SELECT 1", "foo_user")), 1,
|
self.eventuallyEqual(lambda: len(self.query_database_with_user(leader.metadata.name, "postgres", "SELECT 1", "foo_user")), 1,
|
||||||
"Could not connect to the database with rotation user {}".format(rotation_user), 10, 5)
|
"Could not connect to the database with rotation user {}".format(rotation_user), 10, 5)
|
||||||
|
|
||||||
|
# check if rotation has been ignored for user from test_cross_namespace_secrets test
|
||||||
|
db_user_secret = k8s.get_secret(username="test.db_user", namespace="test")
|
||||||
|
secret_username = str(base64.b64decode(db_user_secret.data["username"]), 'utf-8')
|
||||||
|
|
||||||
|
self.assertEqual("test.db_user", secret_username,
|
||||||
|
"Unexpected username in secret of test.db_user: expected {}, got {}".format("test.db_user", secret_username))
|
||||||
|
|
||||||
# disable password rotation for all other users (foo_user)
|
# disable password rotation for all other users (foo_user)
|
||||||
# and pick smaller intervals to see if the third fake rotation user is dropped
|
# and pick smaller intervals to see if the third fake rotation user is dropped
|
||||||
enable_password_rotation = {
|
enable_password_rotation = {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ spec:
|
||||||
- createdb
|
- createdb
|
||||||
foo_user: []
|
foo_user: []
|
||||||
# flyway: []
|
# flyway: []
|
||||||
|
# usersIgnoringSecretRotation:
|
||||||
|
# - bar_user
|
||||||
# usersWithSecretRotation:
|
# usersWithSecretRotation:
|
||||||
# - foo_user
|
# - foo_user
|
||||||
# usersWithInPlaceSecretRotation:
|
# usersWithInPlaceSecretRotation:
|
||||||
|
|
|
||||||
|
|
@ -610,6 +610,11 @@ spec:
|
||||||
- SUPERUSER
|
- SUPERUSER
|
||||||
- nosuperuser
|
- nosuperuser
|
||||||
- NOSUPERUSER
|
- NOSUPERUSER
|
||||||
|
usersIgnoringSecretRotation:
|
||||||
|
type: array
|
||||||
|
nullable: true
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
usersWithInPlaceSecretRotation:
|
usersWithInPlaceSecretRotation:
|
||||||
type: array
|
type: array
|
||||||
nullable: true
|
nullable: true
|
||||||
|
|
|
||||||
|
|
@ -996,6 +996,15 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"usersIgnoringSecretRotation": {
|
||||||
|
Type: "array",
|
||||||
|
Nullable: true,
|
||||||
|
Items: &apiextv1.JSONSchemaPropsOrArray{
|
||||||
|
Schema: &apiextv1.JSONSchemaProps{
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
"usersWithInPlaceSecretRotation": {
|
"usersWithInPlaceSecretRotation": {
|
||||||
Type: "array",
|
Type: "array",
|
||||||
Nullable: true,
|
Nullable: true,
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ type PostgresSpec struct {
|
||||||
AllowedSourceRanges []string `json:"allowedSourceRanges"`
|
AllowedSourceRanges []string `json:"allowedSourceRanges"`
|
||||||
|
|
||||||
Users map[string]UserFlags `json:"users,omitempty"`
|
Users map[string]UserFlags `json:"users,omitempty"`
|
||||||
|
UsersIgnoringSecretRotation []string `json:"usersIgnoringSecretRotation,omitempty"`
|
||||||
UsersWithSecretRotation []string `json:"usersWithSecretRotation,omitempty"`
|
UsersWithSecretRotation []string `json:"usersWithSecretRotation,omitempty"`
|
||||||
UsersWithInPlaceSecretRotation []string `json:"usersWithInPlaceSecretRotation,omitempty"`
|
UsersWithInPlaceSecretRotation []string `json:"usersWithInPlaceSecretRotation,omitempty"`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -718,6 +718,11 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) {
|
||||||
(*out)[key] = outVal
|
(*out)[key] = outVal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if in.UsersIgnoringSecretRotation != nil {
|
||||||
|
in, out := &in.UsersIgnoringSecretRotation, &out.UsersIgnoringSecretRotation
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
if in.UsersWithSecretRotation != nil {
|
if in.UsersWithSecretRotation != nil {
|
||||||
in, out := &in.UsersWithSecretRotation, &out.UsersWithSecretRotation
|
in, out := &in.UsersWithSecretRotation, &out.UsersWithSecretRotation
|
||||||
*out = make([]string, len(*in))
|
*out = make([]string, len(*in))
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/zalando/postgres-operator/pkg/util"
|
"github.com/zalando/postgres-operator/pkg/util"
|
||||||
"github.com/zalando/postgres-operator/pkg/util/constants"
|
"github.com/zalando/postgres-operator/pkg/util/constants"
|
||||||
"github.com/zalando/postgres-operator/pkg/util/k8sutil"
|
"github.com/zalando/postgres-operator/pkg/util/k8sutil"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
batchv1 "k8s.io/api/batch/v1"
|
batchv1 "k8s.io/api/batch/v1"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
policyv1 "k8s.io/api/policy/v1"
|
policyv1 "k8s.io/api/policy/v1"
|
||||||
|
|
@ -689,7 +690,7 @@ func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration(pod *v1.Pod, effectiv
|
||||||
effectiveValue := effectivePgParameters[desiredOption]
|
effectiveValue := effectivePgParameters[desiredOption]
|
||||||
if isBootstrapOnlyParameter(desiredOption) && (effectiveValue != desiredValue) {
|
if isBootstrapOnlyParameter(desiredOption) && (effectiveValue != desiredValue) {
|
||||||
parametersToSet[desiredOption] = desiredValue
|
parametersToSet[desiredOption] = desiredValue
|
||||||
if util.SliceContains(requirePrimaryRestartWhenDecreased, desiredOption) {
|
if slices.Contains(requirePrimaryRestartWhenDecreased, desiredOption) {
|
||||||
effectiveValueNum, errConv := strconv.Atoi(effectiveValue)
|
effectiveValueNum, errConv := strconv.Atoi(effectiveValue)
|
||||||
desiredValueNum, errConv2 := strconv.Atoi(desiredValue)
|
desiredValueNum, errConv2 := strconv.Atoi(desiredValue)
|
||||||
if errConv != nil || errConv2 != nil {
|
if errConv != nil || errConv2 != nil {
|
||||||
|
|
@ -705,7 +706,7 @@ func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration(pod *v1.Pod, effectiv
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if there exist only config updates that require a restart of the primary
|
// check if there exist only config updates that require a restart of the primary
|
||||||
if len(restartPrimary) > 0 && !util.SliceContains(restartPrimary, false) && len(configToSet) == 0 {
|
if len(restartPrimary) > 0 && !slices.Contains(restartPrimary, false) && len(configToSet) == 0 {
|
||||||
requiresMasterRestart = true
|
requiresMasterRestart = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -873,14 +874,17 @@ func (c *Cluster) updateSecret(
|
||||||
// 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
|
||||||
// rotation can be enabled globally or via the manifest (excluding the Postgres superuser)
|
// rotation can be enabled globally or via the manifest (excluding the Postgres superuser)
|
||||||
rotationEnabledInManifest := secretUsername != constants.SuperuserKeyName &&
|
rotationEnabledInManifest := secretUsername != constants.SuperuserKeyName &&
|
||||||
(util.SliceContains(c.Spec.UsersWithSecretRotation, secretUsername) ||
|
(slices.Contains(c.Spec.UsersWithSecretRotation, secretUsername) ||
|
||||||
util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername))
|
slices.Contains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername))
|
||||||
|
|
||||||
// globally enabled rotation is only allowed for manifest and bootstrapped roles
|
// globally enabled rotation is only allowed for manifest and bootstrapped roles
|
||||||
allowedRoleTypes := []spec.RoleOrigin{spec.RoleOriginManifest, spec.RoleOriginBootstrap}
|
allowedRoleTypes := []spec.RoleOrigin{spec.RoleOriginManifest, spec.RoleOriginBootstrap}
|
||||||
rotationAllowed := !pwdUser.IsDbOwner && util.SliceContains(allowedRoleTypes, pwdUser.Origin) && c.Spec.StandbyCluster == nil
|
rotationAllowed := !pwdUser.IsDbOwner && slices.Contains(allowedRoleTypes, pwdUser.Origin) && c.Spec.StandbyCluster == nil
|
||||||
|
|
||||||
if (c.OpConfig.EnablePasswordRotation && rotationAllowed) || rotationEnabledInManifest {
|
// users can ignore any kind of rotation
|
||||||
|
isIgnoringRotation := slices.Contains(c.Spec.UsersIgnoringSecretRotation, secretUsername)
|
||||||
|
|
||||||
|
if ((c.OpConfig.EnablePasswordRotation && rotationAllowed) || rotationEnabledInManifest) && !isIgnoringRotation {
|
||||||
updateSecretMsg, err = c.rotatePasswordInSecret(secret, secretUsername, pwdUser.Origin, currentTime, retentionUsers)
|
updateSecretMsg, err = c.rotatePasswordInSecret(secret, secretUsername, pwdUser.Origin, currentTime, retentionUsers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Warnf("password rotation failed for user %s: %v", secretUsername, err)
|
c.logger.Warnf("password rotation failed for user %s: %v", secretUsername, err)
|
||||||
|
|
@ -961,7 +965,7 @@ func (c *Cluster) rotatePasswordInSecret(
|
||||||
// update password and next rotation date if configured interval has passed
|
// update password and next rotation date if configured interval has passed
|
||||||
if currentTime.After(nextRotationDate) {
|
if currentTime.After(nextRotationDate) {
|
||||||
// create rotation user if role is not listed for in-place password update
|
// create rotation user if role is not listed for in-place password update
|
||||||
if !util.SliceContains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername) {
|
if !slices.Contains(c.Spec.UsersWithInPlaceSecretRotation, secretUsername) {
|
||||||
rotationUsername := fmt.Sprintf("%s%s", secretUsername, currentTime.Format(constants.RotationUserDateFormat))
|
rotationUsername := fmt.Sprintf("%s%s", secretUsername, currentTime.Format(constants.RotationUserDateFormat))
|
||||||
secret.Data["username"] = []byte(rotationUsername)
|
secret.Data["username"] = []byte(rotationUsername)
|
||||||
c.logger.Infof("updating username in secret %s and creating rotation user %s in the database", secretName, rotationUsername)
|
c.logger.Infof("updating username in secret %s and creating rotation user %s in the database", secretName, rotationUsername)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
|
@ -634,7 +635,8 @@ func TestUpdateSecret(t *testing.T) {
|
||||||
},
|
},
|
||||||
Spec: acidv1.PostgresSpec{
|
Spec: acidv1.PostgresSpec{
|
||||||
Databases: map[string]string{dbname: dbowner},
|
Databases: map[string]string{dbname: dbowner},
|
||||||
Users: map[string]acidv1.UserFlags{"foo": {}, dbowner: {}},
|
Users: map[string]acidv1.UserFlags{"foo": {}, "bar": {}, dbowner: {}},
|
||||||
|
UsersIgnoringSecretRotation: []string{"bar"},
|
||||||
UsersWithInPlaceSecretRotation: []string{dbowner},
|
UsersWithInPlaceSecretRotation: []string{dbowner},
|
||||||
Streams: []acidv1.Stream{
|
Streams: []acidv1.Stream{
|
||||||
{
|
{
|
||||||
|
|
@ -712,6 +714,9 @@ func TestUpdateSecret(t *testing.T) {
|
||||||
if pgUser.Origin != spec.RoleOriginManifest {
|
if pgUser.Origin != spec.RoleOriginManifest {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if slices.Contains(pg.Spec.UsersIgnoringSecretRotation, username) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
t.Errorf("%s: password unchanged in updated secret for %s", testName, username)
|
t.Errorf("%s: password unchanged in updated secret for %s", testName, username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue