-
Notifications
You must be signed in to change notification settings - Fork 265
CNTRLPLANE-3237: Carry referenced Secret data from API to plugin lifecycle for KMS #2212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -116,7 +116,9 @@ func NewKeyController( | |
| WithInformers( | ||
| apiServerInformer.Informer(), | ||
| operatorClient.Informer(), | ||
|
|
||
| kubeInformersForNamespaces.InformersFor("openshift-config-managed").Core().V1().Secrets().Informer(), | ||
| // TODO: add informer for openshift-config namespace to watch referenced Secrets for KMS plugin secret data changes | ||
| deployer, | ||
| ).ToController( | ||
| c.controllerInstanceName, | ||
|
|
@@ -223,7 +225,7 @@ func (c *keyController) checkAndCreateKeys(ctx context.Context, syncContext fact | |
|
|
||
| sort.Sort(sort.StringSlice(reasons)) | ||
| internalReason := strings.Join(reasons, ", ") | ||
| keySecret, err := c.generateKeySecret(newKeyID, currentMode, apiEncryptionConfiguration, internalReason, externalReason) | ||
| keySecret, err := c.generateKeySecret(ctx, newKeyID, currentMode, apiEncryptionConfiguration, internalReason, externalReason) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to create key: %v", err) | ||
| } | ||
|
|
@@ -260,7 +262,7 @@ func (c *keyController) validateExistingSecret(ctx context.Context, keySecret *c | |
| return nil // we made this key earlier | ||
| } | ||
|
|
||
| func (c *keyController) generateKeySecret(keyID uint64, currentMode state.Mode, apiServerEncryption configv1.APIServerEncryption, internalReason, externalReason string) (*corev1.Secret, error) { | ||
| func (c *keyController) generateKeySecret(ctx context.Context, keyID uint64, currentMode state.Mode, apiServerEncryption configv1.APIServerEncryption, internalReason, externalReason string) (*corev1.Secret, error) { | ||
| bs := crypto.ModeToNewKeyFunc[currentMode]() | ||
| ks := state.KeyState{ | ||
| Key: apiserverv1.Key{ | ||
|
|
@@ -281,6 +283,22 @@ func (c *keyController) generateKeySecret(keyID uint64, currentMode state.Mode, | |
| }, | ||
| Plugin: apiServerEncryption.KMS, | ||
| } | ||
|
|
||
| if secretName, expectedKeys := referencedSecretName(apiServerEncryption.KMS); len(secretName) > 0 { | ||
| refSecret, err := c.secretClient.Secrets("openshift-config").Get(ctx, secretName, metav1.GetOptions{}) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to get secret %s in openshift-config: %w", secretName, err) | ||
| } | ||
| sd := make(map[string][]byte, len(expectedKeys)) | ||
| for _, key := range expectedKeys { | ||
| v, ok := refSecret.Data[key] | ||
| if !ok { | ||
| return nil, fmt.Errorf("secret %s in openshift-config is missing required key %q", secretName, key) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. worth adding unit tests to covert his scenario ? |
||
| } | ||
| sd[secretName+"-"+key] = v | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what if
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From the encryption controller's perspective this is not a problem. It concatenates secretName and key and propagates to the plugin lifecycle as is. But this collision can be a problem in plugin lifecycle side. I'd like to discuss more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what if: secretName="vault-approle", key="secret-role-id" gives "vault-approle-secret-role-id"
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in the future we might ref more than one secret. |
||
| } | ||
| ks.KMS.SecretData = sd | ||
| } | ||
| } | ||
| return secrets.FromKeyState(c.instanceName, ks) | ||
| } | ||
|
|
@@ -383,6 +401,15 @@ func needsNewKey(grKeys state.GroupResourceState, currentMode state.Mode, extern | |
| return latestKeyID, "rotation-interval-has-passed", time.Since(latestKey.Migrated.Timestamp) > encryptionSecretMigrationInterval | ||
| } | ||
|
|
||
| func referencedSecretName(plugin configv1.KMSPluginConfig) (string, []string) { | ||
| switch plugin.Type { | ||
| case configv1.VaultKMSProvider: | ||
| return plugin.Vault.Authentication.AppRole.Secret.Name, []string{"role-id", "secret-id"} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it is important that we document that we only care respect []string{"role-id", "secret-id"} |
||
| default: | ||
| return "", nil | ||
| } | ||
| } | ||
|
|
||
| // TODO make this un-settable once set | ||
| // ex: we could require the tech preview no upgrade flag to be set before we will honor this field | ||
| type unsupportedEncryptionConfig struct { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,10 @@ type Config struct { | |
| // KMSPlugins maps keyID to plugin-specific configuration, | ||
| // carried from Key Secrets into the encryption-config Secret. | ||
| KMSPlugins map[string]configv1.KMSPluginConfig | ||
| // KMSSecretData maps keyID to secret data carried from | ||
| // Key Secrets into the encryption-config Secret. | ||
| // Inner map keys use the short form (e.g. "app-role-role-id"). | ||
| KMSSecretData map[string]map[string][]byte | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe we should rename to |
||
| } | ||
|
|
||
| func (c *Config) HasEncryptionConfiguration() bool { | ||
|
|
@@ -40,18 +44,19 @@ func (c *Config) HasEncryptionConfiguration() bool { | |
| func FromEncryptionState(encryptionState map[schema.GroupResource]state.GroupResourceState) (*Config, error) { | ||
| resourceConfigs := make([]apiserverconfigv1.ResourceConfiguration, 0, len(encryptionState)) | ||
| var kmsPlugins map[string]configv1.KMSPluginConfig | ||
| var kmsSecretData map[string]map[string][]byte | ||
|
|
||
| for gr, grKeys := range encryptionState { | ||
| resourceConfigs = append(resourceConfigs, apiserverconfigv1.ResourceConfiguration{ | ||
| Resources: []string{gr.String()}, // we are forced to lose data here because this API is broken | ||
| Providers: stateToProviders(gr.Resource, grKeys), | ||
| }) | ||
|
|
||
| // Collect KMS plugin configs from read keys (which already include the write key). | ||
| // We iterate over encryptionState which is keyed by GroupResource, so the same | ||
| // keyID is seen once per resource (e.g. key "1" for secrets and key "1" for configmaps). | ||
| // Since all resources share the same Key Secret, the plugin config is identical | ||
| // across duplicates and we only need to keep the first occurrence. | ||
| // Collect KMS plugin configs and secret data from read keys (which already | ||
| // include the write key). We iterate over encryptionState which is keyed by | ||
| // GroupResource, so the same keyID is seen once per resource. Since all | ||
| // resources share the same Key Secret, the plugin config and secret data are | ||
| // identical across duplicates and we only need to keep the first occurrence. | ||
| for _, key := range grKeys.ReadKeys { | ||
| if key.HasKMSPlugin() { | ||
| if kmsPlugins == nil { | ||
|
|
@@ -67,6 +72,18 @@ func FromEncryptionState(encryptionState map[schema.GroupResource]state.GroupRes | |
| kmsPlugins[key.Key.Name] = key.KMS.Plugin | ||
| } | ||
| } | ||
| if key.HasKMSSecretData() { | ||
| if kmsSecretData == nil { | ||
| kmsSecretData = map[string]map[string][]byte{} | ||
| } | ||
| if existing, exists := kmsSecretData[key.Key.Name]; exists { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we expect duplicates here ? why ?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we don't expect duplicates then maybe we check if there are none. in either case we should add tests.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We expect duplicates. Because each resource will carry their own kms-plugin. Secret's kms-plugin must match to configmap's kms-plugin. I agree that we should cover this in unit tests. |
||
| if !equality.Semantic.DeepEqual(existing, key.KMS.SecretData) { | ||
| return nil, fmt.Errorf("KMS secret data mismatch for keyID %s: secret data from different resources must be identical", key.Key.Name) | ||
| } | ||
| } else { | ||
| kmsSecretData[key.Key.Name] = key.KMS.SecretData | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -76,8 +93,9 @@ func FromEncryptionState(encryptionState map[schema.GroupResource]state.GroupRes | |
| }) | ||
|
|
||
| return &Config{ | ||
| Encryption: &apiserverconfigv1.EncryptionConfiguration{Resources: resourceConfigs}, | ||
| KMSPlugins: kmsPlugins, | ||
| Encryption: &apiserverconfigv1.EncryptionConfiguration{Resources: resourceConfigs}, | ||
| KMSPlugins: kmsPlugins, | ||
| KMSSecretData: kmsSecretData, | ||
| }, nil | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import ( | |
| "fmt" | ||
| "sort" | ||
| "strconv" | ||
| "strings" | ||
|
|
||
| corev1 "k8s.io/api/core/v1" | ||
| "k8s.io/apimachinery/pkg/api/equality" | ||
|
|
@@ -17,11 +18,15 @@ import ( | |
| "github.com/openshift/library-go/pkg/operator/encryption/state" | ||
| ) | ||
|
|
||
| // EncryptionConfSecretName is the name of the final encryption config secret that is revisioned per apiserver rollout. | ||
| const EncryptionConfSecretName = "encryption-config" | ||
|
|
||
| // EncryptionConfSecretKey is the map data key used to store the raw bytes of the final encryption config. | ||
| const EncryptionConfSecretKey = "encryption-config" | ||
| const ( | ||
| // EncryptionConfSecretName is the name of the final encryption config secret that is revisioned per apiserver rollout. | ||
| EncryptionConfSecretName = "encryption-config" | ||
| // EncryptionConfSecretKey is the map data key used to store the raw bytes of the final encryption config. | ||
| EncryptionConfSecretKey = "encryption-config" | ||
| // encryptionConfigSecretDataPrefix is the data key prefix for KMS plugin secret | ||
| // data entries in the encryption-config Secret. Full key: "kms-plugin-secret-{secretDataKey}-{keyID}". | ||
| encryptionConfigSecretDataPrefix = "kms-plugin-secret-" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. worth updating the KEP. |
||
| ) | ||
|
|
||
| func FromSecret(encryptionConfigSecret *corev1.Secret) (*Config, error) { | ||
| data, ok := encryptionConfigSecret.Data[EncryptionConfSecretKey] | ||
|
|
@@ -56,7 +61,29 @@ func FromSecret(encryptionConfigSecret *corev1.Secret) (*Config, error) { | |
| kmsPlugins[keyID] = pluginConfig | ||
| } | ||
|
|
||
| return &Config{Encryption: encryptionConfig, KMSPlugins: kmsPlugins}, nil | ||
| // Extract secret data entries from the encryption-config Secret. | ||
| // Data keys follow the format "kms-plugin-secret-{secretDataKey}-{keyID}" | ||
| // (e.g. "kms-plugin-secret-app-role-role-id-1"). KeyIDFromSecretDataKey | ||
| // returns the keyID (e.g. "1") and the secretDataKey (e.g. "app-role-role-id"). | ||
| var kmsSecretData map[string]map[string][]byte | ||
| for key, value := range encryptionConfigSecret.Data { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we have tests that cover changes in this file ?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. seems like not everything is covered. |
||
| keyID, secretDataKey, found, err := keyIDFromSecretDataKey(key) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to extract keyID from secret data key %s: %w", key, err) | ||
| } | ||
| if !found { | ||
| continue | ||
| } | ||
| if kmsSecretData == nil { | ||
| kmsSecretData = map[string]map[string][]byte{} | ||
| } | ||
| if kmsSecretData[keyID] == nil { | ||
| kmsSecretData[keyID] = map[string][]byte{} | ||
| } | ||
| kmsSecretData[keyID][secretDataKey] = value | ||
| } | ||
|
|
||
| return &Config{Encryption: encryptionConfig, KMSPlugins: kmsPlugins, KMSSecretData: kmsSecretData}, nil | ||
| } | ||
|
|
||
| func ToSecret(ns, name string, secretData *Config) (*corev1.Secret, error) { | ||
|
|
@@ -100,6 +127,19 @@ func ToSecret(ns, name string, secretData *Config) (*corev1.Secret, error) { | |
| s.Data[dataKey] = encodedPlugin | ||
| } | ||
|
|
||
| // Write secret data entries to the encryption-config Secret. | ||
| // Each secret dataKey (e.g. "app-role-role-id") is combined with the keyID | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what if the keyID=123 ?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WDYM?. It will be
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what if dataKey=token-123 ?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please add unit test to cover all edge cases. |
||
| // (e.g. "1") to produce the data key "kms-plugin-secret-app-role-role-id-1". | ||
| for keyID, keySecretData := range secretData.KMSSecretData { | ||
| for secretDataKey, value := range keySecretData { | ||
| dataKey, err := toSecretDataKeyFor(secretDataKey, keyID) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| s.Data[dataKey] = value | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we have the same dataKey from two different secrets then I think we have a collision here. |
||
| } | ||
| } | ||
|
|
||
| return s, nil | ||
| } | ||
|
|
||
|
|
@@ -145,3 +185,26 @@ func ExtractUniqueAndSortedKMSConfigurations(secretData *Config) ([]*apiserverco | |
| }) | ||
| return result, nil | ||
| } | ||
|
|
||
| func toSecretDataKeyFor(secretDataKey, keyID string) (string, error) { | ||
| if _, err := strconv.ParseUint(keyID, 10, 64); err != nil { | ||
| return "", fmt.Errorf("invalid keyID %q: must be a non-negative integer", keyID) | ||
| } | ||
| return encryptionConfigSecretDataPrefix + secretDataKey + "-" + keyID, nil | ||
| } | ||
|
|
||
| func keyIDFromSecretDataKey(dataKey string) (string, string, bool, error) { | ||
| rest, found := strings.CutPrefix(dataKey, encryptionConfigSecretDataPrefix) | ||
| if !found { | ||
| return "", "", false, nil | ||
| } | ||
| i := strings.LastIndex(rest, "-") | ||
| if i < 1 { | ||
| return "", "", false, nil | ||
| } | ||
| keyID := rest[i+1:] | ||
| if _, err := strconv.ParseUint(keyID, 10, 64); err != nil { | ||
| return "", "", false, fmt.Errorf("invalid keyID %q: must be a non-negative integer", keyID) | ||
| } | ||
| return keyID, rest[:i], true, nil | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.