Skip to content

Commit 61e711f

Browse files
Merge pull request #478 from xek/fernet-rotation
Add fernet key rotation cronjob
2 parents f3611e3 + c05fc7b commit 61e711f

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)