Skip to content

Commit 9615b6e

Browse files
afaranhaxek
andcommitted
Support rotation and variable number of Fernet keys
Add configuration for specifying the number of fernet keys stored in the keystone secret. More than 2 keys are needed, since rotating 2 keys would expire sessions on every rotation. After configuration change, keys need to be added/removed and rotated in the proper order, to ensure that the sessions don't expire prematurely. Fernet key rotation is triggered in the reconcile loop. The "rotated at" timestamp is set in the secret annotation. Co-Authored-By: Grzegorz Grasza <[email protected]>
1 parent f3611e3 commit 9615b6e

File tree

12 files changed

+191
-30
lines changed

12 files changed

+191
-30
lines changed

api/bases/keystone.openstack.org_keystoneapis.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,20 @@ spec:
8989
description: EnableSecureRBAC - Enable Consistent and Secure RBAC
9090
policies
9191
type: boolean
92+
fernetMaxActiveKeys:
93+
default: 5
94+
description: FernetMaxActiveKeys - Maximum number of fernet token
95+
keys after rotation
96+
format: int32
97+
minimum: 3
98+
type: integer
99+
fernetRotationDays:
100+
default: 1
101+
description: FernetRotationDays - Rotate fernet token keys every X
102+
days
103+
format: int32
104+
minimum: 1
105+
type: integer
92106
memcachedInstance:
93107
default: memcached
94108
description: Memcached instance name.

api/v1beta1/keystoneapi_types.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,18 @@ type KeystoneAPISpecCore struct {
119119
// TrustFlushSuspend - Suspend the cron job to purge trusts
120120
TrustFlushSuspend bool `json:"trustFlushSuspend"`
121121

122+
// +kubebuilder:validation:Optional
123+
// +kubebuilder:default=1
124+
// +kubebuilder:validation:Minimum=1
125+
// FernetRotationDays - Rotate fernet token keys every X days
126+
FernetRotationDays *int32 `json:"fernetRotationDays"`
127+
128+
// +kubebuilder:validation:Optional
129+
// +kubebuilder:default=5
130+
// +kubebuilder:validation:Minimum=3
131+
// FernetMaxActiveKeys - Maximum number of fernet token keys after rotation
132+
FernetMaxActiveKeys *int32 `json:"fernetMaxActiveKeys"`
133+
122134
// +kubebuilder:validation:Optional
123135
// +kubebuilder:default={admin: AdminPassword}
124136
// PasswordSelectors - Selectors to identify the AdminUser password from the Secret

api/v1beta1/zz_generated.deepcopy.go

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

config/crd/bases/keystone.openstack.org_keystoneapis.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,20 @@ spec:
8989
description: EnableSecureRBAC - Enable Consistent and Secure RBAC
9090
policies
9191
type: boolean
92+
fernetMaxActiveKeys:
93+
default: 5
94+
description: FernetMaxActiveKeys - Maximum number of fernet token
95+
keys after rotation
96+
format: int32
97+
minimum: 3
98+
type: integer
99+
fernetRotationDays:
100+
default: 1
101+
description: FernetRotationDays - Rotate fernet token keys every X
102+
days
103+
format: int32
104+
minimum: 1
105+
type: integer
92106
memcachedInstance:
93107
default: memcached
94108
description: Memcached instance name.

controllers/keystoneapi_controller.go

Lines changed: 109 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,6 @@ func (r *KeystoneAPIReconciler) reconcileNormal(
857857
//
858858
// Create secret holding fernet keys (for token and credential)
859859
//
860-
// TODO key rotation
861860
err = r.ensureFernetKeys(ctx, instance, helper, &configMapVars)
862861
if err != nil {
863862
instance.Status.Conditions.Set(condition.FalseCondition(
@@ -1321,37 +1320,48 @@ func (r *KeystoneAPIReconciler) reconcileCloudConfig(
13211320
return oko_secret.EnsureSecrets(ctx, h, instance, secrets, nil)
13221321
}
13231322

1324-
// ensureFernetKeys - creates secret with fernet keys
1323+
// ensureFernetKeys - creates secret with fernet keys, rotates the keys
13251324
func (r *KeystoneAPIReconciler) ensureFernetKeys(
13261325
ctx context.Context,
13271326
instance *keystonev1.KeystoneAPI,
13281327
helper *helper.Helper,
13291328
envVars *map[string]env.Setter,
13301329
) error {
1330+
fernetAnnotation := labels.GetGroupLabel(keystone.ServiceName) + "/rotatedat"
13311331
labels := labels.GetLabels(instance, labels.GetGroupLabel(keystone.ServiceName), map[string]string{})
1332+
now := time.Now().UTC()
13321333

13331334
//
13341335
// check if secret already exist
13351336
//
13361337
secretName := keystone.ServiceName
1338+
numberKeys := int(*instance.Spec.FernetMaxActiveKeys)
1339+
13371340
secret, hash, err := oko_secret.GetSecret(ctx, helper, secretName, instance.Namespace)
1341+
13381342
if err != nil && !k8s_errors.IsNotFound(err) {
13391343
return err
13401344
} else if k8s_errors.IsNotFound(err) {
13411345
fernetKeys := map[string]string{
1342-
"FernetKeys0": keystone.GenerateFernetKey(),
1343-
"FernetKeys1": keystone.GenerateFernetKey(),
13441346
"CredentialKeys0": keystone.GenerateFernetKey(),
13451347
"CredentialKeys1": keystone.GenerateFernetKey(),
13461348
}
13471349

1350+
for i := 0; i < numberKeys; i++ {
1351+
fernetKeys[fmt.Sprintf("FernetKeys%d", i)] = keystone.GenerateFernetKey()
1352+
}
1353+
1354+
annotations := map[string]string{
1355+
fernetAnnotation: now.Format(time.RFC3339)}
1356+
13481357
tmpl := []util.Template{
13491358
{
1350-
Name: secretName,
1351-
Namespace: instance.Namespace,
1352-
Type: util.TemplateTypeNone,
1353-
CustomData: fernetKeys,
1354-
Labels: labels,
1359+
Name: secretName,
1360+
Namespace: instance.Namespace,
1361+
Type: util.TemplateTypeNone,
1362+
CustomData: fernetKeys,
1363+
Labels: labels,
1364+
Annotations: annotations,
13551365
},
13561366
}
13571367
err := oko_secret.EnsureSecrets(ctx, helper, instance, tmpl, envVars)
@@ -1361,9 +1371,97 @@ func (r *KeystoneAPIReconciler) ensureFernetKeys(
13611371
} else {
13621372
// add hash to envVars
13631373
(*envVars)[secret.Name] = env.SetValue(hash)
1364-
}
13651374

1366-
// TODO: fernet key rotation
1375+
changedKeys := false
1376+
1377+
extraKey := fmt.Sprintf("FernetKeys%d", numberKeys)
1378+
1379+
//
1380+
// Fernet Key rotation
1381+
//
1382+
rotatedAt, err := time.Parse(time.RFC3339, secret.Annotations[fernetAnnotation])
1383+
duration := int(*instance.Spec.FernetRotationDays)
1384+
1385+
if err != nil {
1386+
secret.Annotations[fernetAnnotation] = now.Format(time.RFC3339)
1387+
changedKeys = true
1388+
} else if rotatedAt.AddDate(0, 0, duration).Before(now) {
1389+
secret.Data[extraKey] = secret.Data["FernetKeys0"]
1390+
secret.Data["FernetKeys0"] = []byte(keystone.GenerateFernetKey())
1391+
}
1392+
1393+
//
1394+
// Remove extra keys when FernetMaxActiveKeys changes
1395+
//
1396+
for {
1397+
_, exists := secret.Data[extraKey]
1398+
if !exists {
1399+
break
1400+
}
1401+
changedKeys = true
1402+
i := 1
1403+
for {
1404+
key := fmt.Sprintf("FernetKeys%d", i)
1405+
i++
1406+
nextKey := fmt.Sprintf("FernetKeys%d", i)
1407+
_, exists = secret.Data[nextKey]
1408+
if !exists {
1409+
break
1410+
}
1411+
secret.Data[key] = secret.Data[nextKey]
1412+
delete(secret.Data, nextKey)
1413+
}
1414+
}
1415+
1416+
//
1417+
// Add extra keys when FernetMaxActiveKeys changes
1418+
//
1419+
lastKey := fmt.Sprintf("FernetKeys%d", numberKeys-1)
1420+
for {
1421+
_, exists := secret.Data[lastKey]
1422+
if exists {
1423+
break
1424+
}
1425+
changedKeys = true
1426+
i := 1
1427+
nextKeyValue := []byte(keystone.GenerateFernetKey())
1428+
for {
1429+
key := fmt.Sprintf("FernetKeys%d", i)
1430+
i++
1431+
keyValue, exists := secret.Data[key]
1432+
secret.Data[key] = nextKeyValue
1433+
nextKeyValue = keyValue
1434+
if !exists {
1435+
break
1436+
}
1437+
}
1438+
}
1439+
1440+
if !changedKeys {
1441+
return nil
1442+
}
1443+
1444+
fernetKeys := make(map[string]string, len(secret.Data))
1445+
for k, v := range secret.Data {
1446+
fernetKeys[k] = string(v[:])
1447+
}
1448+
1449+
tmpl := []util.Template{
1450+
{
1451+
Name: secretName,
1452+
Namespace: instance.Namespace,
1453+
Type: util.TemplateTypeNone,
1454+
CustomData: fernetKeys,
1455+
Labels: labels,
1456+
Annotations: secret.Annotations,
1457+
},
1458+
}
1459+
1460+
err = oko_secret.EnsureSecrets(ctx, helper, instance, tmpl, envVars)
1461+
if err != nil {
1462+
return err
1463+
}
1464+
}
13671465

13681466
return nil
13691467
}

pkg/keystone/bootstrap.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,12 @@ func BootstrapJob(
6060
}
6161

6262
// create Volume and VolumeMounts
63-
volumes := getVolumes(instance.Name)
63+
volumes := getVolumes(instance)
6464
volumeMounts := getVolumeMounts()
6565

6666
// add CA cert if defined
6767
if instance.Spec.TLS.CaBundleSecretName != "" {
68-
volumes = append(getVolumes(instance.Name), instance.Spec.TLS.CreateVolume())
68+
volumes = append(getVolumes(instance), instance.Spec.TLS.CreateVolume())
6969
volumeMounts = append(getVolumeMounts(), instance.Spec.TLS.CreateVolumeMounts(nil)...)
7070
}
7171

pkg/keystone/cronjob.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ func CronJob(
4646
completions := int32(1)
4747

4848
// create Volume and VolumeMounts
49-
volumes := getVolumes(instance.Name)
49+
volumes := getVolumes(instance)
5050
volumeMounts := getVolumeMounts()
5151

5252
// add CA cert if defined
5353
if instance.Spec.TLS.CaBundleSecretName != "" {
54-
volumes = append(getVolumes(instance.Name), instance.Spec.TLS.CreateVolume())
54+
volumes = append(getVolumes(instance), instance.Spec.TLS.CreateVolume())
5555
volumeMounts = append(getVolumeMounts(), instance.Spec.TLS.CreateVolumeMounts(nil)...)
5656
}
5757

pkg/keystone/dbsync.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,13 @@ func DbSyncJob(
4545
envVars["KOLLA_BOOTSTRAP"] = env.SetValue("true")
4646

4747
// create Volume and VolumeMounts
48-
volumes := getVolumes(instance.Name)
48+
volumes := getVolumes(instance)
4949
volumeMounts := getVolumeMounts()
5050

5151
// add CA cert if defined
5252
if instance.Spec.TLS.CaBundleSecretName != "" {
53-
volumes = append(getVolumes(instance.Name), instance.Spec.TLS.CreateVolume())
53+
//TODO(afaranha): Why not reuse the 'volumes'?
54+
volumes = append(getVolumes(instance), instance.Spec.TLS.CreateVolume())
5455
volumeMounts = append(getVolumeMounts(), instance.Spec.TLS.CreateVolumeMounts(nil)...)
5556
}
5657

pkg/keystone/deployment.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func Deployment(
8080
envVars["CONFIG_HASH"] = env.SetValue(configHash)
8181

8282
// create Volume and VolumeMounts
83-
volumes := getVolumes(instance.Name)
83+
volumes := getVolumes(instance)
8484
volumeMounts := getVolumeMounts()
8585

8686
// add CA cert if defined

pkg/keystone/fernet.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package keystone
1717

1818
import (
1919
"encoding/base64"
20-
2120
"math/rand"
2221
)
2322

0 commit comments

Comments
 (0)