Skip to content

Commit 9a0295d

Browse files
committed
Introduce KMS encryption mode in encryption library
1 parent f489e81 commit 9a0295d

File tree

14 files changed

+876
-19
lines changed

14 files changed

+876
-19
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ require (
3434
golang.org/x/net v0.43.0
3535
golang.org/x/sys v0.36.0
3636
golang.org/x/time v0.9.0
37+
google.golang.org/grpc v1.72.1
3738
gopkg.in/evanphx/json-patch.v4 v4.12.0
3839
gopkg.in/natefinch/lumberjack.v2 v2.2.1
3940
k8s.io/api v0.34.1
@@ -43,6 +44,7 @@ require (
4344
k8s.io/client-go v0.34.1
4445
k8s.io/component-base v0.34.1
4546
k8s.io/klog/v2 v2.130.1
47+
k8s.io/kms v0.34.1
4648
k8s.io/kube-aggregator v0.34.1
4749
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
4850
sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96
@@ -125,11 +127,9 @@ require (
125127
golang.org/x/tools v0.36.0 // indirect
126128
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
127129
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
128-
google.golang.org/grpc v1.72.1 // indirect
129130
google.golang.org/protobuf v1.36.5 // indirect
130131
gopkg.in/inf.v0 v0.9.1 // indirect
131132
gopkg.in/yaml.v3 v3.0.1 // indirect
132-
k8s.io/kms v0.34.1 // indirect
133133
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
134134
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
135135
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect

pkg/operator/encryption/controllers/key_controller.go

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111
"time"
1212

13+
"github.com/openshift/library-go/pkg/operator/encryption/kms"
1314
corev1 "k8s.io/api/core/v1"
1415
"k8s.io/apimachinery/pkg/api/errors"
1516
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -20,6 +21,7 @@ import (
2021
"k8s.io/klog/v2"
2122
"k8s.io/utils/ptr"
2223

24+
configv1 "github.com/openshift/api/config/v1"
2325
operatorv1 "github.com/openshift/api/operator/v1"
2426
configv1client "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1"
2527
configv1informers "github.com/openshift/client-go/config/informers/externalversions/config/v1"
@@ -159,11 +161,20 @@ func (c *keyController) sync(ctx context.Context, syncCtx factory.SyncContext) (
159161
}
160162

161163
func (c *keyController) checkAndCreateKeys(ctx context.Context, syncContext factory.SyncContext, encryptedGRs []schema.GroupResource) error {
162-
currentMode, externalReason, err := c.getCurrentModeAndExternalReason(ctx)
164+
currentMode, externalReason, kmsConfig, err := c.getCurrentModeAndExternalReason(ctx)
163165
if err != nil {
164166
return err
165167
}
166168

169+
// Compute KMS hashes if using KMS mode
170+
var kmsConfigHash, kmsKeyIDHash string
171+
if currentMode == state.KMS && kmsConfig != nil {
172+
kmsConfigHash, kmsKeyIDHash, err = c.getKMSHashes(ctx, kmsConfig)
173+
if err != nil {
174+
return err
175+
}
176+
}
177+
167178
currentConfig, desiredEncryptionState, secrets, isProgressingReason, err := statemachine.GetEncryptionConfigAndState(ctx, c.deployer, c.secretClient, c.encryptionSecretSelector, encryptedGRs)
168179
if err != nil {
169180
return err
@@ -191,7 +202,7 @@ func (c *keyController) checkAndCreateKeys(ctx context.Context, syncContext fact
191202

192203
var commonReason *string
193204
for gr, grKeys := range desiredEncryptionState {
194-
latestKeyID, internalReason, needed := needsNewKey(grKeys, currentMode, externalReason, encryptedGRs)
205+
latestKeyID, internalReason, needed := needsNewKey(grKeys, currentMode, externalReason, encryptedGRs, kmsConfigHash, kmsKeyIDHash)
195206
if !needed {
196207
continue
197208
}
@@ -218,7 +229,7 @@ func (c *keyController) checkAndCreateKeys(ctx context.Context, syncContext fact
218229

219230
sort.Sort(sort.StringSlice(reasons))
220231
internalReason := strings.Join(reasons, ", ")
221-
keySecret, err := c.generateKeySecret(newKeyID, currentMode, internalReason, externalReason)
232+
keySecret, err := c.generateKeySecret(newKeyID, currentMode, internalReason, externalReason, kmsConfigHash, kmsKeyIDHash)
222233
if err != nil {
223234
return fmt.Errorf("failed to create key: %v", err)
224235
}
@@ -255,7 +266,7 @@ func (c *keyController) validateExistingSecret(ctx context.Context, keySecret *c
255266
return nil // we made this key earlier
256267
}
257268

258-
func (c *keyController) generateKeySecret(keyID uint64, currentMode state.Mode, internalReason, externalReason string) (*corev1.Secret, error) {
269+
func (c *keyController) generateKeySecret(keyID uint64, currentMode state.Mode, internalReason, externalReason, kmsConfigHash, kmsKeyIDHash string) (*corev1.Secret, error) {
259270
bs := crypto.ModeToNewKeyFunc[currentMode]()
260271
ks := state.KeyState{
261272
Key: apiserverv1.Key{
@@ -265,40 +276,69 @@ func (c *keyController) generateKeySecret(keyID uint64, currentMode state.Mode,
265276
Mode: currentMode,
266277
InternalReason: internalReason,
267278
ExternalReason: externalReason,
279+
KMSConfigHash: kmsConfigHash,
280+
KMSKeyIDHash: kmsKeyIDHash,
268281
}
269282
return secrets.FromKeyState(c.instanceName, ks)
270283
}
271284

272-
func (c *keyController) getCurrentModeAndExternalReason(ctx context.Context) (state.Mode, string, error) {
285+
func (c *keyController) getCurrentModeAndExternalReason(ctx context.Context) (state.Mode, string, *configv1.KMSConfig, error) {
273286
apiServer, err := c.apiServerClient.Get(ctx, "cluster", metav1.GetOptions{})
274287
if err != nil {
275-
return "", "", err
288+
return "", "", nil, err
276289
}
277290

278291
operatorSpec, _, _, err := c.operatorClient.GetOperatorState()
279292
if err != nil {
280-
return "", "", err
293+
return "", "", nil, err
281294
}
282295

283296
encryptionConfig, err := structuredUnsupportedConfigFrom(operatorSpec.UnsupportedConfigOverrides.Raw, c.unsupportedConfigPrefix)
284297
if err != nil {
285-
return "", "", err
298+
return "", "", nil, err
286299
}
287300

288301
reason := encryptionConfig.Encryption.Reason
289302
switch currentMode := state.Mode(apiServer.Spec.Encryption.Type); currentMode {
290303
case state.AESCBC, state.AESGCM, state.Identity: // secretbox is disabled for now
291-
return currentMode, reason, nil
304+
return currentMode, reason, nil, nil
305+
case state.KMS:
306+
return currentMode, reason, apiServer.Spec.Encryption.KMS, nil
292307
case "": // unspecified means use the default (which can change over time)
293-
return state.DefaultMode, reason, nil
308+
return state.DefaultMode, reason, nil, nil
294309
default:
295-
return "", "", fmt.Errorf("unknown encryption mode configured: %s", currentMode)
310+
return "", "", nil, fmt.Errorf("unknown encryption mode configured: %s", currentMode)
311+
}
312+
}
313+
314+
func (c *keyController) getKMSHashes(ctx context.Context, kmsConfig *configv1.KMSConfig) (string, string, error) {
315+
// Generate unix socket path from KMS config and get the hash
316+
socketPath, configHash, err := kms.GenerateUnixSocketPath(kmsConfig)
317+
if err != nil {
318+
return "", "", fmt.Errorf("failed to generate KMS unix socket path: %w", err)
319+
}
320+
321+
kmsClient, err := kms.NewKMSClient(socketPath)
322+
if err != nil {
323+
return "", "", fmt.Errorf("failed to create KMS client: %w", err)
324+
}
325+
defer kmsClient.Close()
326+
327+
statusResp, err := kmsClient.Status(ctx)
328+
if err != nil {
329+
return "", "", fmt.Errorf("failed to call KMS Status endpoint: %w", err)
330+
}
331+
332+
if statusResp.Healthz != "ok" {
333+
return "", "", fmt.Errorf("KMS plugin is unhealthy: %s", statusResp.Healthz)
296334
}
335+
336+
return configHash, kms.ComputeKMSKeyIDHash(statusResp.KeyID), nil
297337
}
298338

299339
// needsNewKey checks whether a new key must be created for the given resource. If true, it also returns the latest
300340
// used key ID and a reason string.
301-
func needsNewKey(grKeys state.GroupResourceState, currentMode state.Mode, externalReason string, encryptedGRs []schema.GroupResource) (uint64, string, bool) {
341+
func needsNewKey(grKeys state.GroupResourceState, currentMode state.Mode, externalReason string, encryptedGRs []schema.GroupResource, kmsConfigHash, kmsKeyIDHash string) (uint64, string, bool) {
302342
// we always need to have some encryption keys unless we are turned off
303343
if len(grKeys.ReadKeys) == 0 {
304344
return 0, "key-does-not-exist", currentMode != state.Identity
@@ -346,6 +386,21 @@ func needsNewKey(grKeys state.GroupResourceState, currentMode state.Mode, extern
346386
return latestKeyID, "external-reason-changed", true
347387
}
348388

389+
// if we are using KMS, check if the KMS configuration or key ID hash has changed
390+
if currentMode == state.KMS {
391+
if latestKey.KMSConfigHash != kmsConfigHash && len(kmsConfigHash) != 0 {
392+
return latestKeyID, "kms-config-changed", true
393+
}
394+
395+
if latestKey.KMSKeyIDHash != kmsKeyIDHash && len(kmsKeyIDHash) != 0 {
396+
return latestKeyID, "kms-key-id-changed", true
397+
}
398+
399+
// For KMS mode, we don't do time-based rotation
400+
// KMS keys are rotated externally by the KMS system
401+
return 0, "", false
402+
}
403+
349404
// we check for encryptionSecretMigratedTimestamp set by migration controller to determine when migration completed
350405
// this also generates back pressure for key rotation when migration takes a long time or was recently completed
351406
return latestKeyID, "rotation-interval-has-passed", time.Since(latestKey.Migrated.Timestamp) > encryptionSecretMigrationInterval

pkg/operator/encryption/controllers/key_controller_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ func TestGetCurrentModeAndExternalReason(t *testing.T) {
495495

496496
// act
497497
target := keyController{unsupportedConfigPrefix: scenario.prefix, operatorClient: fakeOperatorClient, apiServerClient: fakeApiServerClient}
498-
_, externalReason, err := target.getCurrentModeAndExternalReason(context.TODO())
498+
_, externalReason, _, err := target.getCurrentModeAndExternalReason(context.TODO())
499499

500500
// validate
501501
if err != nil {

pkg/operator/encryption/controllers/state_controller_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,78 @@ func TestStateController(t *testing.T) {
707707
encryptiontesting.ValidateOperatorClientConditions(ts, operatorClient, []operatorv1.OperatorCondition{expectedCondition})
708708
},
709709
},
710+
{
711+
name: "secret with EncryptionConfig is created for KMS mode with proper KMS provider configuration",
712+
targetNamespace: "kms",
713+
targetGRs: []schema.GroupResource{
714+
{Group: "", Resource: "secrets"},
715+
},
716+
initialResources: []runtime.Object{
717+
encryptiontesting.CreateDummyKubeAPIPod("kube-apiserver-1", "kms", "node-1"),
718+
func() *corev1.Secret {
719+
s := encryptiontesting.CreateEncryptionKeySecretNoDataWithMode("kms", []schema.GroupResource{{Group: "", Resource: "secrets"}}, 1, "kms")
720+
// Set KMS-specific annotations with config hash and key ID hash
721+
s.Annotations["encryption.apiserver.operator.openshift.io/kms-config-hash"] = "1234567890abcdef"
722+
s.Annotations["encryption.apiserver.operator.openshift.io/kms-key-id-hash"] = "fedcba0987654321"
723+
return s
724+
}(),
725+
},
726+
expectedActions: []string{
727+
"list:pods:kms",
728+
"get:secrets:kms",
729+
"list:secrets:openshift-config-managed",
730+
"get:secrets:openshift-config-managed",
731+
"create:secrets:openshift-config-managed",
732+
"create:events:kms",
733+
"create:events:kms",
734+
},
735+
expectedEncryptionCfg: func() *apiserverconfigv1.EncryptionConfiguration {
736+
return &apiserverconfigv1.EncryptionConfiguration{
737+
TypeMeta: metav1.TypeMeta{
738+
Kind: "EncryptionConfiguration",
739+
APIVersion: "apiserver.config.k8s.io/v1",
740+
},
741+
Resources: []apiserverconfigv1.ResourceConfiguration{
742+
{
743+
Resources: []string{"secrets"},
744+
Providers: []apiserverconfigv1.ProviderConfiguration{
745+
{
746+
Identity: &apiserverconfigv1.IdentityConfiguration{},
747+
},
748+
{
749+
KMS: &apiserverconfigv1.KMSConfiguration{
750+
APIVersion: "v2",
751+
Name: "kms-provider-fedcba0987654321-1", // kms-provider-{keyIDHash16}-{key ID}
752+
Endpoint: "unix://var/run/kms/kms-1234567890abcdef.sock",
753+
Timeout: &metav1.Duration{
754+
Duration: 10 * time.Second,
755+
},
756+
},
757+
},
758+
},
759+
},
760+
},
761+
}
762+
}(),
763+
validateFunc: func(ts *testing.T, actions []clientgotesting.Action, destName string, expectedEncryptionCfg *apiserverconfigv1.EncryptionConfiguration) {
764+
wasSecretValidated := false
765+
for _, action := range actions {
766+
if action.Matches("create", "secrets") {
767+
createAction := action.(clientgotesting.CreateAction)
768+
actualSecret := createAction.GetObject().(*corev1.Secret)
769+
err := validateSecretWithEncryptionConfig(actualSecret, expectedEncryptionCfg, destName)
770+
if err != nil {
771+
ts.Fatalf("failed to verify the encryption config, due to %v", err)
772+
}
773+
wasSecretValidated = true
774+
break
775+
}
776+
}
777+
if !wasSecretValidated {
778+
ts.Errorf("the secret wasn't created and validated")
779+
}
780+
},
781+
},
710782
}
711783

712784
for _, scenario := range scenarios {

pkg/operator/encryption/crypto/keys.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var (
1212
state.AESGCM: NewAES256Key,
1313
state.SecretBox: NewAES256Key, // secretbox requires a 32 byte key so we can reuse the same function here
1414
state.Identity: NewIdentityKey,
15+
state.KMS: NewIdentityKey, // this is not used in KMS
1516
}
1617
)
1718

pkg/operator/encryption/encryptionconfig/config.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/base64"
55
"sort"
66

7+
"github.com/openshift/library-go/pkg/operator/encryption/kms"
78
corev1 "k8s.io/api/core/v1"
89
"k8s.io/apimachinery/pkg/runtime/schema"
910
apiserverconfigv1 "k8s.io/apiserver/pkg/apis/apiserver/v1"
@@ -106,6 +107,25 @@ func ToEncryptionState(encryptionConfig *apiserverconfigv1.EncryptionConfigurati
106107
Mode: s,
107108
}
108109

110+
case provider.KMS != nil:
111+
configHash, keyIDHash, keyName, err := kms.ExtractKMSHashAndKeyName(provider)
112+
if err != nil {
113+
klog.Warningf("skipping invalid encryption KMS config for resource %v", provider)
114+
continue // should never happen
115+
}
116+
117+
ks = state.KeyState{
118+
Key: apiserverconfigv1.Key{
119+
Name: keyName,
120+
// We set this unused secret just to align with what we set initially.
121+
// This is unused.
122+
Secret: base64.StdEncoding.EncodeToString(crypto.ModeToNewKeyFunc[state.KMS]()),
123+
},
124+
Mode: state.KMS,
125+
KMSConfigHash: configHash,
126+
KMSKeyIDHash: keyIDHash,
127+
}
128+
109129
default:
110130
klog.Infof("skipping invalid provider index %d for resource %s", i, resourceConfig.Resources[0])
111131
continue // should never happen
@@ -192,6 +212,8 @@ func stateToProviders(desired state.GroupResourceState) []apiserverconfigv1.Prov
192212
Keys: []apiserverconfigv1.Key{key.Key},
193213
},
194214
})
215+
case state.KMS:
216+
providers = append(providers, kms.GenerateKMSProviderConfigurationFromKey(key))
195217
default:
196218
// this should never happen because our input should always be valid
197219
klog.Infof("skipping key %s as it has invalid mode %s", key.Key.Name, key.Mode)

0 commit comments

Comments
 (0)