diff --git a/controllers/keystoneapi_controller.go b/controllers/keystoneapi_controller.go index 1ce29856..4f05a2d2 100644 --- a/controllers/keystoneapi_controller.go +++ b/controllers/keystoneapi_controller.go @@ -1189,7 +1189,8 @@ func (r *KeystoneAPIReconciler) generateServiceConfigMaps( instance.Status.DatabaseHostname, keystone.DatabaseName, ), - "enableSecureRBAC": instance.Spec.EnableSecureRBAC, + "enableSecureRBAC": instance.Spec.EnableSecureRBAC, + "fernetMaxActiveKeys": instance.Spec.FernetMaxActiveKeys, } // create httpd vhost template parameters @@ -1454,11 +1455,6 @@ func (r *KeystoneAPIReconciler) ensureFernetKeys( return nil } - fernetKeys := make(map[string]string, len(secret.Data)) - for k, v := range secret.Data { - fernetKeys[k] = string(v[:]) - } - secret.Annotations[fernetAnnotation] = now.Format(time.RFC3339) // use update to apply changes to the secret, since EnsureSecrets diff --git a/templates/keystoneapi/config/keystone.conf b/templates/keystoneapi/config/keystone.conf index 6717aa26..6e4cc2e0 100644 --- a/templates/keystoneapi/config/keystone.conf +++ b/templates/keystoneapi/config/keystone.conf @@ -22,7 +22,7 @@ enforce_scope = {{ .enableSecureRBAC }} [fernet_tokens] key_repository=/etc/keystone/fernet-keys -max_active_keys=2 +max_active_keys={{ .fernetMaxActiveKeys }} {{ if (index . "TransportURL") }} [oslo_messaging_notifications] diff --git a/tests/functional/base_test.go b/tests/functional/base_test.go index 5e07cc38..f318d672 100644 --- a/tests/functional/base_test.go +++ b/tests/functional/base_test.go @@ -30,15 +30,20 @@ import ( condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" ) -func GetDefaultKeystoneAPISpec() map[string]interface{} { +func GetKeystoneAPISpec(fernetMaxKeys int32) map[string]interface{} { return map[string]interface{}{ - "databaseInstance": "openstack", - "replicas": 1, - "secret": SecretName, - "databaseAccount": AccountName, + "databaseInstance": "openstack", + "replicas": 1, + "secret": SecretName, + "databaseAccount": AccountName, + "fernetMaxActiveKeys": fernetMaxKeys, } } +func GetDefaultKeystoneAPISpec() map[string]interface{} { + return GetKeystoneAPISpec(5) +} + func GetTLSKeystoneAPISpec() map[string]interface{} { return map[string]interface{}{ "databaseInstance": "openstack", diff --git a/tests/functional/keystoneapi_controller_test.go b/tests/functional/keystoneapi_controller_test.go index 905b0341..d375ee47 100644 --- a/tests/functional/keystoneapi_controller_test.go +++ b/tests/functional/keystoneapi_controller_test.go @@ -20,6 +20,7 @@ import ( "fmt" "os" "strconv" + "strings" "time" . "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports @@ -36,6 +37,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) var _ = Describe("Keystone controller", func() { @@ -1109,6 +1111,248 @@ var _ = Describe("Keystone controller", func() { }) }) + When("When FernetMaxActiveKeys is created with a number lower than 3", func() { + It("should fail", func() { + err := InterceptGomegaFailure( + func() { + CreateKeystoneAPI(keystoneAPIName, GetKeystoneAPISpec(-1)) + }) + Expect(err).Should(HaveOccurred()) + }) + }) + + When("When the fernet keys are created with FernetMaxActiveKeys as 3", func() { + BeforeEach(func() { + DeferCleanup( + k8sClient.Delete, ctx, CreateKeystoneMessageBusSecret(namespace, "rabbitmq-secret")) + DeferCleanup(th.DeleteInstance, CreateKeystoneAPI(keystoneAPIName, GetKeystoneAPISpec(3))) + DeferCleanup( + k8sClient.Delete, ctx, CreateKeystoneAPISecret(namespace, SecretName)) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, "memcached", memcachedSpec)) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + namespace, + GetKeystoneAPI(keystoneAPIName).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + mariadb.SimulateMariaDBAccountCompleted(keystoneAccountName) + mariadb.SimulateMariaDBDatabaseCompleted(keystoneDatabaseName) + infra.SimulateTransportURLReady(types.NamespacedName{ + Name: fmt.Sprintf("%s-keystone-transport", keystoneAPIName.Name), + Namespace: namespace, + }) + infra.SimulateMemcachedReady(types.NamespacedName{ + Name: "memcached", + Namespace: namespace, + }) + th.SimulateJobSuccess(dbSyncJobName) + th.SimulateJobSuccess(bootstrapJobName) + th.SimulateDeploymentReplicaReady(deploymentName) + }) + + It("creates 3 keys", func() { + secret := th.GetSecret(types.NamespacedName{Namespace: keystoneAPIName.Namespace, Name: "keystone"}) + Expect(secret).ToNot(BeNil()) + + Eventually(func(g Gomega) { + numberFernetKeys := 0 + for k := range secret.Data { + if strings.HasPrefix(k, "FernetKeys") { + numberFernetKeys++ + } + } + + Expect(numberFernetKeys).Should(BeNumerically("==", 3)) + for i := 0; i < 3; i++ { + g.Expect(secret.Data["FernetKeys"+strconv.Itoa(i)]).NotTo(BeNil()) + } + }, timeout, interval).Should(Succeed()) + }) + }) + + When("When the fernet keys are created with FernetMaxActiveKeys as 100", func() { + BeforeEach(func() { + DeferCleanup( + k8sClient.Delete, ctx, CreateKeystoneMessageBusSecret(namespace, "rabbitmq-secret")) + DeferCleanup(th.DeleteInstance, CreateKeystoneAPI(keystoneAPIName, GetKeystoneAPISpec(100))) + DeferCleanup( + k8sClient.Delete, ctx, CreateKeystoneAPISecret(namespace, SecretName)) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, "memcached", memcachedSpec)) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + namespace, + GetKeystoneAPI(keystoneAPIName).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + mariadb.SimulateMariaDBAccountCompleted(keystoneAccountName) + mariadb.SimulateMariaDBDatabaseCompleted(keystoneDatabaseName) + infra.SimulateTransportURLReady(types.NamespacedName{ + Name: fmt.Sprintf("%s-keystone-transport", keystoneAPIName.Name), + Namespace: namespace, + }) + infra.SimulateMemcachedReady(types.NamespacedName{ + Name: "memcached", + Namespace: namespace, + }) + th.SimulateJobSuccess(dbSyncJobName) + th.SimulateJobSuccess(bootstrapJobName) + th.SimulateDeploymentReplicaReady(deploymentName) + }) + + It("creates 100 keys", func() { + secret := th.GetSecret(types.NamespacedName{Namespace: keystoneAPIName.Namespace, Name: "keystone"}) + Expect(secret).ToNot(BeNil()) + + Eventually(func(g Gomega) { + numberFernetKeys := 0 + for k := range secret.Data { + if strings.HasPrefix(k, "FernetKeys") { + numberFernetKeys++ + } + } + + Expect(numberFernetKeys).Should(BeNumerically("==", 100)) + for i := 0; i < 100; i++ { + g.Expect(secret.Data["FernetKeys"+strconv.Itoa(i)]).NotTo(BeNil()) + } + }, timeout, interval).Should(Succeed()) + }) + }) + + When("When the fernet keys are updated from 5 to 4", func() { + BeforeEach(func() { + DeferCleanup( + k8sClient.Delete, ctx, CreateKeystoneMessageBusSecret(namespace, "rabbitmq-secret")) + DeferCleanup(th.DeleteInstance, CreateKeystoneAPI(keystoneAPIName, GetDefaultKeystoneAPISpec())) + DeferCleanup( + k8sClient.Delete, ctx, CreateKeystoneAPISecret(namespace, SecretName)) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, "memcached", memcachedSpec)) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + namespace, + GetKeystoneAPI(keystoneAPIName).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + mariadb.SimulateMariaDBAccountCompleted(keystoneAccountName) + mariadb.SimulateMariaDBDatabaseCompleted(keystoneDatabaseName) + infra.SimulateTransportURLReady(types.NamespacedName{ + Name: fmt.Sprintf("%s-keystone-transport", keystoneAPIName.Name), + Namespace: namespace, + }) + infra.SimulateMemcachedReady(types.NamespacedName{ + Name: "memcached", + Namespace: namespace, + }) + th.SimulateJobSuccess(dbSyncJobName) + th.SimulateJobSuccess(bootstrapJobName) + th.SimulateDeploymentReplicaReady(deploymentName) + }) + + It("removes the additional key", func() { + secret := th.GetSecret(types.NamespacedName{Namespace: keystoneAPIName.Namespace, Name: "keystone"}) + Expect(secret).ToNot(BeNil()) + + keystone := GetKeystoneAPI(keystoneAPIName) + + _, err := controllerutil.CreateOrPatch( + th.Ctx, th.K8sClient, keystone, func() error { + keystone.Spec.FernetMaxActiveKeys = ptr.To(int32(4)) + return nil + }) + Expect(err).ToNot(HaveOccurred()) + + Eventually(func(g Gomega) { + secret = th.GetSecret(types.NamespacedName{Namespace: keystoneAPIName.Namespace, Name: "keystone"}) + numberFernetKeys := 0 + for k := range secret.Data { + if strings.HasPrefix(k, "FernetKeys") { + numberFernetKeys++ + } + } + + g.Expect(numberFernetKeys).Should(BeNumerically("==", 4)) + for i := 0; i < 4; i++ { + g.Expect(secret.Data["FernetKeys"+strconv.Itoa(i)]).NotTo(BeNil()) + } + }, timeout, interval).Should(Succeed()) + }) + }) + + When("When the fernet keys are updated from 5 to 6", func() { + BeforeEach(func() { + DeferCleanup( + k8sClient.Delete, ctx, CreateKeystoneMessageBusSecret(namespace, "rabbitmq-secret")) + DeferCleanup(th.DeleteInstance, CreateKeystoneAPI(keystoneAPIName, GetDefaultKeystoneAPISpec())) + DeferCleanup( + k8sClient.Delete, ctx, CreateKeystoneAPISecret(namespace, SecretName)) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, "memcached", memcachedSpec)) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + namespace, + GetKeystoneAPI(keystoneAPIName).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + mariadb.SimulateMariaDBAccountCompleted(keystoneAccountName) + mariadb.SimulateMariaDBDatabaseCompleted(keystoneDatabaseName) + infra.SimulateTransportURLReady(types.NamespacedName{ + Name: fmt.Sprintf("%s-keystone-transport", keystoneAPIName.Name), + Namespace: namespace, + }) + infra.SimulateMemcachedReady(types.NamespacedName{ + Name: "memcached", + Namespace: namespace, + }) + th.SimulateJobSuccess(dbSyncJobName) + th.SimulateJobSuccess(bootstrapJobName) + th.SimulateDeploymentReplicaReady(deploymentName) + }) + + It("creates an additional key", func() { + secret := th.GetSecret(types.NamespacedName{Namespace: keystoneAPIName.Namespace, Name: "keystone"}) + Expect(secret).ToNot(BeNil()) + + keystone := GetKeystoneAPI(keystoneAPIName) + + _, err := controllerutil.CreateOrPatch( + th.Ctx, th.K8sClient, keystone, func() error { + keystone.Spec.FernetMaxActiveKeys = ptr.To(int32(6)) + return nil + }) + Expect(err).ToNot(HaveOccurred()) + + Eventually(func(g Gomega) { + secret = th.GetSecret(types.NamespacedName{Namespace: keystoneAPIName.Namespace, Name: "keystone"}) + numberFernetKeys := 0 + for k := range secret.Data { + if strings.HasPrefix(k, "FernetKeys") { + numberFernetKeys++ + } + } + + g.Expect(numberFernetKeys).Should(BeNumerically("==", 6)) + for i := 0; i < 6; i++ { + g.Expect(secret.Data["FernetKeys"+strconv.Itoa(i)]).NotTo(BeNil()) + } + }, timeout, interval).Should(Succeed()) + }) + }) + // Set rotated at to past date, triggering rotation When("When the fernet token rotate", func() { BeforeEach(func() {