Skip to content

Commit abef62d

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

File tree

11 files changed

+738
-19
lines changed

11 files changed

+738
-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: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ import (
2020
"k8s.io/klog/v2"
2121
"k8s.io/utils/ptr"
2222

23+
configv1 "github.com/openshift/api/config/v1"
2324
operatorv1 "github.com/openshift/api/operator/v1"
2425
configv1client "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1"
2526
configv1informers "github.com/openshift/client-go/config/informers/externalversions/config/v1"
2627
applyoperatorv1 "github.com/openshift/client-go/operator/applyconfigurations/operator/v1"
2728

2829
"github.com/openshift/library-go/pkg/controller/factory"
2930
"github.com/openshift/library-go/pkg/operator/encryption/crypto"
31+
"github.com/openshift/library-go/pkg/operator/encryption/kms"
3032
"github.com/openshift/library-go/pkg/operator/encryption/secrets"
3133
"github.com/openshift/library-go/pkg/operator/encryption/state"
3234
"github.com/openshift/library-go/pkg/operator/encryption/statemachine"
@@ -159,11 +161,46 @@ 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, err = kms.ComputeKMSConfigHash(kmsConfig)
173+
if err != nil {
174+
return fmt.Errorf("failed to compute KMS config hash: %w", err)
175+
}
176+
177+
// Generate unix socket path from KMS config
178+
socketPath, err := kms.GenerateUnixSocketPath(kmsConfig)
179+
if err != nil {
180+
return fmt.Errorf("failed to generate KMS unix socket path: %w", err)
181+
}
182+
183+
// Create KMS client and call Status endpoint
184+
kmsClient, err := kms.NewKMSClient(ctx, socketPath)
185+
if err != nil {
186+
return fmt.Errorf("failed to create KMS client: %w", err)
187+
}
188+
defer kmsClient.Close()
189+
190+
statusResp, err := kmsClient.Status(ctx)
191+
if err != nil {
192+
return fmt.Errorf("failed to call KMS Status endpoint: %w", err)
193+
}
194+
195+
// Check if KMS plugin is healthy
196+
if statusResp.Healthz != "ok" {
197+
return fmt.Errorf("KMS plugin is unhealthy: %s", statusResp.Healthz)
198+
}
199+
200+
// Compute hash of the KeyID returned by KMS
201+
kmsKeyIDHash = kms.ComputeKMSKeyIDHash(statusResp.KeyID)
202+
}
203+
167204
currentConfig, desiredEncryptionState, secrets, isProgressingReason, err := statemachine.GetEncryptionConfigAndState(ctx, c.deployer, c.secretClient, c.encryptionSecretSelector, encryptedGRs)
168205
if err != nil {
169206
return err
@@ -191,7 +228,7 @@ func (c *keyController) checkAndCreateKeys(ctx context.Context, syncContext fact
191228

192229
var commonReason *string
193230
for gr, grKeys := range desiredEncryptionState {
194-
latestKeyID, internalReason, needed := needsNewKey(grKeys, currentMode, externalReason, encryptedGRs)
231+
latestKeyID, internalReason, needed := needsNewKey(grKeys, currentMode, externalReason, encryptedGRs, kmsConfigHash, kmsKeyIDHash)
195232
if !needed {
196233
continue
197234
}
@@ -218,7 +255,7 @@ func (c *keyController) checkAndCreateKeys(ctx context.Context, syncContext fact
218255

219256
sort.Sort(sort.StringSlice(reasons))
220257
internalReason := strings.Join(reasons, ", ")
221-
keySecret, err := c.generateKeySecret(newKeyID, currentMode, internalReason, externalReason)
258+
keySecret, err := c.generateKeySecret(newKeyID, currentMode, internalReason, externalReason, kmsConfigHash, kmsKeyIDHash)
222259
if err != nil {
223260
return fmt.Errorf("failed to create key: %v", err)
224261
}
@@ -255,7 +292,7 @@ func (c *keyController) validateExistingSecret(ctx context.Context, keySecret *c
255292
return nil // we made this key earlier
256293
}
257294

258-
func (c *keyController) generateKeySecret(keyID uint64, currentMode state.Mode, internalReason, externalReason string) (*corev1.Secret, error) {
295+
func (c *keyController) generateKeySecret(keyID uint64, currentMode state.Mode, internalReason, externalReason, kmsConfigHash, kmsKeyIDHash string) (*corev1.Secret, error) {
259296
bs := crypto.ModeToNewKeyFunc[currentMode]()
260297
ks := state.KeyState{
261298
Key: apiserverv1.Key{
@@ -265,40 +302,44 @@ func (c *keyController) generateKeySecret(keyID uint64, currentMode state.Mode,
265302
Mode: currentMode,
266303
InternalReason: internalReason,
267304
ExternalReason: externalReason,
305+
KMSConfigHash: kmsConfigHash,
306+
KMSKeyIDHash: kmsKeyIDHash,
268307
}
269308
return secrets.FromKeyState(c.instanceName, ks)
270309
}
271310

272-
func (c *keyController) getCurrentModeAndExternalReason(ctx context.Context) (state.Mode, string, error) {
311+
func (c *keyController) getCurrentModeAndExternalReason(ctx context.Context) (state.Mode, string, *configv1.KMSConfig, error) {
273312
apiServer, err := c.apiServerClient.Get(ctx, "cluster", metav1.GetOptions{})
274313
if err != nil {
275-
return "", "", err
314+
return "", "", nil, err
276315
}
277316

278317
operatorSpec, _, _, err := c.operatorClient.GetOperatorState()
279318
if err != nil {
280-
return "", "", err
319+
return "", "", nil, err
281320
}
282321

283322
encryptionConfig, err := structuredUnsupportedConfigFrom(operatorSpec.UnsupportedConfigOverrides.Raw, c.unsupportedConfigPrefix)
284323
if err != nil {
285-
return "", "", err
324+
return "", "", nil, err
286325
}
287326

288327
reason := encryptionConfig.Encryption.Reason
289328
switch currentMode := state.Mode(apiServer.Spec.Encryption.Type); currentMode {
290329
case state.AESCBC, state.AESGCM, state.Identity: // secretbox is disabled for now
291-
return currentMode, reason, nil
330+
return currentMode, reason, nil, nil
331+
case state.KMS:
332+
return currentMode, reason, apiServer.Spec.Encryption.KMS, nil
292333
case "": // unspecified means use the default (which can change over time)
293-
return state.DefaultMode, reason, nil
334+
return state.DefaultMode, reason, nil, nil
294335
default:
295-
return "", "", fmt.Errorf("unknown encryption mode configured: %s", currentMode)
336+
return "", "", nil, fmt.Errorf("unknown encryption mode configured: %s", currentMode)
296337
}
297338
}
298339

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

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

pkg/operator/encryption/controllers/key_controller_test.go

Lines changed: 45 additions & 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 {
@@ -507,3 +507,47 @@ func TestGetCurrentModeAndExternalReason(t *testing.T) {
507507
})
508508
}
509509
}
510+
511+
func TestGetCurrentModeAndExternalReasonWithKMS(t *testing.T) {
512+
apiServerWithKMS := &configv1.APIServer{
513+
ObjectMeta: metav1.ObjectMeta{Name: "cluster"},
514+
Spec: configv1.APIServerSpec{
515+
Encryption: configv1.APIServerEncryption{
516+
Type: "kms",
517+
KMS: &configv1.KMSConfig{
518+
Type: configv1.AWSKMSProvider,
519+
AWS: &configv1.AWSKMSConfig{
520+
KeyARN: "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012",
521+
Region: "us-east-1",
522+
},
523+
},
524+
},
525+
},
526+
}
527+
528+
fakeOperatorClient := v1helpers.NewFakeStaticPodOperatorClient(
529+
&operatorv1.StaticPodOperatorSpec{
530+
OperatorSpec: operatorv1.OperatorSpec{},
531+
}, &operatorv1.StaticPodOperatorStatus{}, nil, nil,
532+
)
533+
fakeConfigClient := configv1clientfake.NewSimpleClientset(apiServerWithKMS)
534+
fakeApiServerClient := fakeConfigClient.ConfigV1().APIServers()
535+
536+
// act
537+
target := keyController{operatorClient: fakeOperatorClient, apiServerClient: fakeApiServerClient}
538+
mode, _, kmsConfig, err := target.getCurrentModeAndExternalReason(context.TODO())
539+
540+
// validate
541+
if err != nil {
542+
t.Errorf("unexpected error: %v", err)
543+
}
544+
if mode != "kms" {
545+
t.Errorf("expected mode to be 'kms', got %q", mode)
546+
}
547+
if kmsConfig == nil {
548+
t.Error("expected kmsConfig to be returned, got nil")
549+
}
550+
if kmsConfig != nil && kmsConfig.Type != configv1.AWSKMSProvider {
551+
t.Errorf("expected AWS KMS provider, got %v", kmsConfig.Type)
552+
}
553+
}

pkg/operator/encryption/encryptionconfig/config.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"k8s.io/klog/v2"
1111

1212
"github.com/openshift/library-go/pkg/operator/encryption/crypto"
13+
"github.com/openshift/library-go/pkg/operator/encryption/kms"
1314
"github.com/openshift/library-go/pkg/operator/encryption/secrets"
1415
"github.com/openshift/library-go/pkg/operator/encryption/state"
1516
)
@@ -106,6 +107,18 @@ func ToEncryptionState(encryptionConfig *apiserverconfigv1.EncryptionConfigurati
106107
Mode: s,
107108
}
108109

110+
case provider.KMS != nil:
111+
// Extract the config hash from the KMS endpoint path
112+
configHash := kms.ExtractHashFromSocketPath(provider.KMS.Endpoint)
113+
ks = state.KeyState{
114+
Key: apiserverconfigv1.Key{
115+
// TODO: we must set key.Name
116+
Secret: "",
117+
},
118+
Mode: state.KMS,
119+
KMSConfigHash: configHash,
120+
}
121+
109122
default:
110123
klog.Infof("skipping invalid provider index %d for resource %s", i, resourceConfig.Resources[0])
111124
continue // should never happen
@@ -192,6 +205,17 @@ func stateToProviders(desired state.GroupResourceState) []apiserverconfigv1.Prov
192205
Keys: []apiserverconfigv1.Key{key.Key},
193206
},
194207
})
208+
case state.KMS:
209+
// Generate unix socket endpoint from the config hash
210+
endpoint := kms.GenerateUnixSocketPathFromHash(key.KMSConfigHash)
211+
providerName := "kms-provider-" + key.KMSConfigHash[:8]
212+
providers = append(providers, apiserverconfigv1.ProviderConfiguration{
213+
KMS: &apiserverconfigv1.KMSConfiguration{
214+
APIVersion: "v2",
215+
Name: providerName,
216+
Endpoint: endpoint,
217+
},
218+
})
195219
default:
196220
// this should never happen because our input should always be valid
197221
klog.Infof("skipping key %s as it has invalid mode %s", key.Key.Name, key.Mode)

0 commit comments

Comments
 (0)