From 24fdd44f1c9d0ed569c351429e29f70de53f32c2 Mon Sep 17 00:00:00 2001 From: sandeepknd Date: Fri, 15 May 2026 18:27:18 +0530 Subject: [PATCH] added vault key rotation func --- .../encryption/kms/vault_key_rotation.go | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 test/library/encryption/kms/vault_key_rotation.go diff --git a/test/library/encryption/kms/vault_key_rotation.go b/test/library/encryption/kms/vault_key_rotation.go new file mode 100644 index 0000000000..861b811970 --- /dev/null +++ b/test/library/encryption/kms/vault_key_rotation.go @@ -0,0 +1,169 @@ +package kms + +import ( + "context" + "encoding/json" + "fmt" + "os/exec" + "time" + + "k8s.io/client-go/kubernetes" +) + +const ( + // DefaultVaultNamespace is the default namespace where Vault runs + DefaultVaultNamespace = "vault-kms" + + // DefaultVaultPodName is the default Vault pod name in StatefulSet + DefaultVaultPodName = "vault-0" + + // DefaultTransitKeyName is the default transit encryption key name + DefaultTransitKeyName = "kms-key" + + // vaultCommandTimeout is the timeout for vault oc exec commands + // to prevent indefinite blocking in e2e test runs + vaultCommandTimeout = 30 * time.Second +) + +// KeyRotator provides an interface for rotating encryption keys. +// This interface allows different key rotation implementations (Vault, etcd, etc.) +// to be used interchangeably in test helpers. +type KeyRotator interface { + // ForceKeyRotation rotates the encryption key and forces immediate use of the new key. + // Returns error if rotation fails. + ForceKeyRotation(ctx context.Context) error +} + +// VaultKeyRotator handles Vault transit key rotation operations +type VaultKeyRotator struct { + kubeClient kubernetes.Interface + namespace string + podName string + transitKeyName string +} + +// NewVaultKeyRotator creates a new VaultKeyRotator with default values +func NewVaultKeyRotator(kubeClient kubernetes.Interface) *VaultKeyRotator { + return &VaultKeyRotator{ + kubeClient: kubeClient, + namespace: DefaultVaultNamespace, + podName: DefaultVaultPodName, + transitKeyName: DefaultTransitKeyName, + } +} + +// NewVaultKeyRotatorWithConfig creates a new VaultKeyRotator with custom configuration +func NewVaultKeyRotatorWithConfig(kubeClient kubernetes.Interface, namespace, podName, transitKeyName string) *VaultKeyRotator { + return &VaultKeyRotator{ + kubeClient: kubeClient, + namespace: namespace, + podName: podName, + transitKeyName: transitKeyName, + } +} + +// ForceKeyRotation implements the KeyRotator interface. +// It rotates the Vault transit encryption key. All old key versions are retained. +// +// Reference: https://developer.hashicorp.com/vault/api-docs/secret/transit#rotate-key +// +// Steps: +// 1. Get initial key version +// 2. Execute 'vault write -f transit/keys//rotate' via oc exec +// 3. Get new key version and validate it increased +func (v *VaultKeyRotator) ForceKeyRotation(ctx context.Context) error { + // Get initial version before rotation + initialVersion, err := v.getCurrentKeyVersion(ctx) + if err != nil { + return fmt.Errorf("failed to get initial key version: %w", err) + } + + // Rotate the key + if err := v.rotateKey(ctx); err != nil { + return fmt.Errorf("failed to rotate key: %w", err) + } + + // Get the new key version after rotation + newVersion, err := v.getCurrentKeyVersion(ctx) + if err != nil { + return fmt.Errorf("failed to get new key version: %w", err) + } + + // Validate that rotation actually happened + if newVersion <= initialVersion { + return fmt.Errorf("rotation failed: version did not increase (before=%d, after=%d)", initialVersion, newVersion) + } + + return nil +} + +// GetKeyInfo returns information about the transit key including current version and configuration +func (v *VaultKeyRotator) GetKeyInfo(ctx context.Context) (map[string]interface{}, error) { + // Add timeout to prevent indefinite blocking in e2e test runs + commandCtx, cancel := context.WithTimeout(ctx, vaultCommandTimeout) + defer cancel() + + // Read key information: vault read transit/keys/ + cmd := exec.CommandContext(commandCtx, "oc", "exec", v.podName, "-n", v.namespace, "--", + "vault", "read", "-format=json", fmt.Sprintf("transit/keys/%s", v.transitKeyName)) + + output, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("failed to read key info: %w, output: %s", err, string(output)) + } + + // Parse the JSON output + var result map[string]interface{} + if err := parseJSON(output, &result); err != nil { + return nil, fmt.Errorf("failed to parse key info: %w", err) + } + + return result, nil +} + +// rotateKey executes the vault key rotation command +func (v *VaultKeyRotator) rotateKey(ctx context.Context) error { + // Add timeout to prevent indefinite blocking in e2e test runs + commandCtx, cancel := context.WithTimeout(ctx, vaultCommandTimeout) + defer cancel() + + // Command: vault write -f transit/keys//rotate + // Reference: https://developer.hashicorp.com/vault/api-docs/secret/transit#rotate-key + cmd := exec.CommandContext(commandCtx, "oc", "exec", v.podName, "-n", v.namespace, "--", + "vault", "write", "-f", fmt.Sprintf("transit/keys/%s/rotate", v.transitKeyName)) + + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("vault key rotation failed: %w, output: %s", err, string(output)) + } + + return nil +} + +// getCurrentKeyVersion retrieves the current (latest) key version +func (v *VaultKeyRotator) getCurrentKeyVersion(ctx context.Context) (int, error) { + keyInfo, err := v.GetKeyInfo(ctx) + if err != nil { + return 0, err + } + + data, ok := keyInfo["data"].(map[string]interface{}) + if !ok { + return 0, fmt.Errorf("unexpected key info format: missing data field") + } + + latestVersion, ok := data["latest_version"].(float64) + if !ok { + return 0, fmt.Errorf("unexpected key info format: missing or invalid latest_version") + } + + return int(latestVersion), nil +} + +// parseJSON is a helper to parse JSON output from vault commands +func parseJSON(data []byte, v interface{}) error { + if len(data) == 0 { + return fmt.Errorf("empty JSON data") + } + return json.Unmarshal(data, v) +}