Skip to content

Commit c05fc7b

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 c05fc7b

File tree

14 files changed

+271
-30
lines changed

14 files changed

+271
-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: 116 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,53 @@ 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+
var numberKeys int
1339+
if instance.Spec.FernetMaxActiveKeys == nil {
1340+
numberKeys = keystone.DefaultFernetMaxActiveKeys
1341+
} else {
1342+
numberKeys = int(*instance.Spec.FernetMaxActiveKeys)
1343+
}
1344+
13371345
secret, hash, err := oko_secret.GetSecret(ctx, helper, secretName, instance.Namespace)
1346+
13381347
if err != nil && !k8s_errors.IsNotFound(err) {
13391348
return err
13401349
} else if k8s_errors.IsNotFound(err) {
13411350
fernetKeys := map[string]string{
1342-
"FernetKeys0": keystone.GenerateFernetKey(),
1343-
"FernetKeys1": keystone.GenerateFernetKey(),
13441351
"CredentialKeys0": keystone.GenerateFernetKey(),
13451352
"CredentialKeys1": keystone.GenerateFernetKey(),
13461353
}
13471354

1355+
for i := 0; i < numberKeys; i++ {
1356+
fernetKeys[fmt.Sprintf("FernetKeys%d", i)] = keystone.GenerateFernetKey()
1357+
}
1358+
1359+
annotations := map[string]string{
1360+
fernetAnnotation: now.Format(time.RFC3339)}
1361+
13481362
tmpl := []util.Template{
13491363
{
1350-
Name: secretName,
1351-
Namespace: instance.Namespace,
1352-
Type: util.TemplateTypeNone,
1353-
CustomData: fernetKeys,
1354-
Labels: labels,
1364+
Name: secretName,
1365+
Namespace: instance.Namespace,
1366+
Type: util.TemplateTypeNone,
1367+
CustomData: fernetKeys,
1368+
Labels: labels,
1369+
Annotations: annotations,
13551370
},
13561371
}
13571372
err := oko_secret.EnsureSecrets(ctx, helper, instance, tmpl, envVars)
@@ -1361,9 +1376,99 @@ func (r *KeystoneAPIReconciler) ensureFernetKeys(
13611376
} else {
13621377
// add hash to envVars
13631378
(*envVars)[secret.Name] = env.SetValue(hash)
1364-
}
13651379

1366-
// TODO: fernet key rotation
1380+
changedKeys := false
1381+
1382+
extraKey := fmt.Sprintf("FernetKeys%d", numberKeys)
1383+
1384+
//
1385+
// Fernet Key rotation
1386+
//
1387+
if secret.Annotations == nil {
1388+
secret.Annotations = map[string]string{}
1389+
}
1390+
rotatedAt, err := time.Parse(time.RFC3339, secret.Annotations[fernetAnnotation])
1391+
1392+
var duration int
1393+
if instance.Spec.FernetRotationDays == nil {
1394+
duration = keystone.DefaultFernetRotationDays
1395+
} else {
1396+
duration = int(*instance.Spec.FernetRotationDays)
1397+
}
1398+
1399+
if err != nil {
1400+
changedKeys = true
1401+
} else if rotatedAt.AddDate(0, 0, duration).Before(now) {
1402+
secret.Data[extraKey] = secret.Data["FernetKeys0"]
1403+
secret.Data["FernetKeys0"] = []byte(keystone.GenerateFernetKey())
1404+
}
1405+
1406+
//
1407+
// Remove extra keys when FernetMaxActiveKeys changes
1408+
//
1409+
for {
1410+
_, exists := secret.Data[extraKey]
1411+
if !exists {
1412+
break
1413+
}
1414+
changedKeys = true
1415+
i := 1
1416+
for {
1417+
key := fmt.Sprintf("FernetKeys%d", i)
1418+
i++
1419+
nextKey := fmt.Sprintf("FernetKeys%d", i)
1420+
_, exists = secret.Data[nextKey]
1421+
if !exists {
1422+
break
1423+
}
1424+
secret.Data[key] = secret.Data[nextKey]
1425+
delete(secret.Data, nextKey)
1426+
}
1427+
}
1428+
1429+
//
1430+
// Add extra keys when FernetMaxActiveKeys changes
1431+
//
1432+
lastKey := fmt.Sprintf("FernetKeys%d", numberKeys-1)
1433+
for {
1434+
_, exists := secret.Data[lastKey]
1435+
if exists {
1436+
break
1437+
}
1438+
changedKeys = true
1439+
i := 1
1440+
nextKeyValue := []byte(keystone.GenerateFernetKey())
1441+
for {
1442+
key := fmt.Sprintf("FernetKeys%d", i)
1443+
i++
1444+
keyValue, exists := secret.Data[key]
1445+
secret.Data[key] = nextKeyValue
1446+
nextKeyValue = keyValue
1447+
if !exists {
1448+
break
1449+
}
1450+
}
1451+
}
1452+
1453+
if !changedKeys {
1454+
return nil
1455+
}
1456+
1457+
fernetKeys := make(map[string]string, len(secret.Data))
1458+
for k, v := range secret.Data {
1459+
fernetKeys[k] = string(v[:])
1460+
}
1461+
1462+
secret.Annotations[fernetAnnotation] = now.Format(time.RFC3339)
1463+
1464+
// use update to apply changes to the secret, since EnsureSecrets
1465+
// does not handle annotation updates, also CreateOrPatchSecret would
1466+
// preserve the existing annotation
1467+
err = helper.GetClient().Update(ctx, secret, &client.UpdateOptions{})
1468+
if err != nil {
1469+
return err
1470+
}
1471+
}
13671472

13681473
return nil
13691474
}

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/const.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,9 @@ const (
2828
KeystonePublicPort int32 = 5000
2929
// KeystoneInternalPort -
3030
KeystoneInternalPort int32 = 5000
31+
32+
// DefaultFernetMaxActiveKeys -
33+
DefaultFernetMaxActiveKeys = 5
34+
// DefaultFernetRotationDays -
35+
DefaultFernetRotationDays = 1
3136
)

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

0 commit comments

Comments
 (0)