sc = kekVersionIdSearch.create();
+ sc.setParameters("kekVersionId", kekVersionId);
+ return listBy(sc);
+ }
+}
+
diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
index edc14d9fa0c..a5e4b07931a 100644
--- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
+++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
@@ -312,4 +312,7 @@
+
+
+
diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
index d69b524b85d..ff5651ba38f 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
@@ -63,6 +63,7 @@ CREATE TABLE IF NOT EXISTS `cloud`.`webhook_filter` (
CONSTRAINT `fk_webhook_filter__webhook_id` FOREIGN KEY(`webhook_id`) REFERENCES `webhook`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
-- "api_keypair" table for API and secret keys
CREATE TABLE IF NOT EXISTS `cloud`.`api_keypair` (
`id` bigint(20) unsigned NOT NULL auto_increment,
@@ -114,3 +115,77 @@ CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Resource Admin', 'deleteUserKey
-- Add conserve mode for VPC offerings
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','conserve_mode', 'tinyint(1) unsigned NULL DEFAULT 0 COMMENT ''True if the VPC offering is IP conserve mode enabled, allowing public IP services to be used across multiple VPC tiers'' ');
+
+-- KMS Keys (Key Encryption Key Metadata)
+-- Account-scoped KEKs for envelope encryption
+CREATE TABLE IF NOT EXISTS `cloud`.`kms_keys` (
+ `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique ID',
+ `uuid` VARCHAR(40) NOT NULL COMMENT 'UUID - user-facing identifier',
+ `name` VARCHAR(255) NOT NULL COMMENT 'User-friendly name',
+ `description` VARCHAR(1024) COMMENT 'User description',
+ `kek_label` VARCHAR(255) NOT NULL COMMENT 'Provider-specific KEK label/ID',
+ `purpose` VARCHAR(32) NOT NULL COMMENT 'Key purpose (VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET)',
+ `account_id` BIGINT UNSIGNED NOT NULL COMMENT 'Owning account',
+ `domain_id` BIGINT UNSIGNED NOT NULL COMMENT 'Owning domain',
+ `zone_id` BIGINT UNSIGNED NOT NULL COMMENT 'Zone where key is valid',
+ `provider_name` VARCHAR(64) NOT NULL COMMENT 'KMS provider (database, pkcs11, etc.)',
+ `algorithm` VARCHAR(64) NOT NULL DEFAULT 'AES/GCM/NoPadding' COMMENT 'Encryption algorithm',
+ `key_bits` INT NOT NULL DEFAULT 256 COMMENT 'Key size in bits',
+ `state` VARCHAR(32) NOT NULL DEFAULT 'Enabled' COMMENT 'Enabled, Disabled, or Deleted',
+ `created` DATETIME NOT NULL COMMENT 'Creation timestamp',
+ `removed` DATETIME COMMENT 'Removal timestamp for soft delete',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_uuid` (`uuid`),
+ INDEX `idx_account_purpose` (`account_id`, `purpose`, `state`),
+ INDEX `idx_domain_purpose` (`domain_id`, `purpose`, `state`),
+ INDEX `idx_zone_state` (`zone_id`, `state`),
+ INDEX `idx_kek_label_provider` (`kek_label`, `provider_name`),
+ CONSTRAINT `fk_kms_keys__account_id` FOREIGN KEY (`account_id`) REFERENCES `account`(`id`) ON DELETE CASCADE,
+ CONSTRAINT `fk_kms_keys__domain_id` FOREIGN KEY (`domain_id`) REFERENCES `domain`(`id`) ON DELETE CASCADE,
+ CONSTRAINT `fk_kms_keys__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KMS Key (KEK) metadata - account-scoped keys for envelope encryption';
+
+-- KMS KEK Versions (multiple KEKs per KMS key for gradual rotation)
+-- Supports multiple KEK versions per logical KMS key during rotation
+CREATE TABLE IF NOT EXISTS `cloud`.`kms_kek_versions` (
+ `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique ID',
+ `uuid` VARCHAR(40) NOT NULL COMMENT 'UUID',
+ `kms_key_id` BIGINT UNSIGNED NOT NULL COMMENT 'Reference to kms_keys table',
+ `version_number` INT NOT NULL COMMENT 'Version number (1, 2, 3, ...)',
+ `kek_label` VARCHAR(255) NOT NULL COMMENT 'Provider-specific KEK label/ID for this version',
+ `status` VARCHAR(32) NOT NULL DEFAULT 'Active' COMMENT 'Active, Previous, Archived',
+ `created` DATETIME NOT NULL COMMENT 'Creation timestamp',
+ `removed` DATETIME COMMENT 'Removal timestamp for soft delete',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_uuid` (`uuid`),
+ UNIQUE KEY `uk_kms_key_version` (`kms_key_id`, `version_number`, `removed`),
+ INDEX `idx_kms_key_status` (`kms_key_id`, `status`, `removed`),
+ INDEX `idx_kek_label` (`kek_label`),
+ CONSTRAINT `fk_kms_kek_versions__kms_key_id` FOREIGN KEY (`kms_key_id`) REFERENCES `kms_keys`(`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KEK versions for a KMS key - supports gradual rotation';
+
+-- KMS Wrapped Keys (Data Encryption Keys)
+-- Generic table for wrapped DEKs - references kms_keys for metadata and kek_versions for specific KEK version
+CREATE TABLE IF NOT EXISTS `cloud`.`kms_wrapped_key` (
+ `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique ID',
+ `uuid` VARCHAR(40) NOT NULL COMMENT 'UUID',
+ `kms_key_id` BIGINT UNSIGNED COMMENT 'Reference to kms_keys table',
+ `kek_version_id` BIGINT UNSIGNED COMMENT 'Reference to kms_kek_versions table',
+ `zone_id` BIGINT UNSIGNED NOT NULL COMMENT 'Zone ID for zone-scoped keys',
+ `wrapped_blob` VARBINARY(4096) NOT NULL COMMENT 'Encrypted DEK material',
+ `created` DATETIME NOT NULL COMMENT 'Creation timestamp',
+ `removed` DATETIME COMMENT 'Removal timestamp for soft delete',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_uuid` (`uuid`),
+ INDEX `idx_kms_key_id` (`kms_key_id`, `removed`),
+ INDEX `idx_kek_version_id` (`kek_version_id`, `removed`),
+ INDEX `idx_zone_id` (`zone_id`, `removed`),
+ CONSTRAINT `fk_kms_wrapped_key__kms_key_id` FOREIGN KEY (`kms_key_id`) REFERENCES `kms_keys`(`id`) ON DELETE RESTRICT,
+ CONSTRAINT `fk_kms_wrapped_key__kek_version_id` FOREIGN KEY (`kek_version_id`) REFERENCES `kms_kek_versions`(`id`) ON DELETE RESTRICT,
+ CONSTRAINT `fk_kms_wrapped_key__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KMS wrapped encryption keys (DEKs) - references kms_keys for KEK metadata and kek_versions for specific version';
+
+-- Add KMS wrapped key reference to volumes table
+CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.volumes', 'kms_wrapped_key_id', 'BIGINT UNSIGNED COMMENT ''KMS wrapped key ID for volume encryption''');
+CALL `cloud`.`IDEMPOTENT_ADD_FOREIGN_KEY`('cloud.volumes', 'fk_volumes__kms_wrapped_key_id', '(kms_wrapped_key_id)', '`kms_wrapped_key`(`id`)');
+
diff --git a/framework/kms/pom.xml b/framework/kms/pom.xml
new file mode 100644
index 00000000000..adb7d9bd449
--- /dev/null
+++ b/framework/kms/pom.xml
@@ -0,0 +1,29 @@
+
+
+ 4.0.0
+ cloud-framework-kms
+ Apache CloudStack Framework - Key Management Service
+ Core KMS framework with provider-agnostic interfaces
+
+
+ org.apache.cloudstack
+ cloudstack-framework
+ 4.23.0.0-SNAPSHOT
+ ../pom.xml
+
+
+
+
+ org.apache.cloudstack
+ cloud-utils
+ ${project.version}
+
+
+ org.apache.cloudstack
+ cloud-framework-config
+ ${project.version}
+
+
+
diff --git a/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSException.java b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSException.java
new file mode 100644
index 00000000000..58b8d251a57
--- /dev/null
+++ b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSException.java
@@ -0,0 +1,179 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.framework.kms;
+
+import com.cloud.utils.exception.CloudRuntimeException;
+
+/**
+ * Exception class for KMS-related errors with structured error types
+ * to enable proper retry logic and error handling.
+ */
+public class KMSException extends CloudRuntimeException {
+
+ /**
+ * Error types for KMS operations to enable intelligent retry logic
+ */
+ public enum ErrorType {
+ /**
+ * Provider not initialized or unavailable
+ */
+ PROVIDER_NOT_INITIALIZED(false),
+
+ /**
+ * KEK not found in backend
+ */
+ KEK_NOT_FOUND(false),
+
+ /**
+ * KEK with given label already exists
+ */
+ KEY_ALREADY_EXISTS(false),
+
+ /**
+ * Invalid parameters provided
+ */
+ INVALID_PARAMETER(false),
+
+ /**
+ * Wrap/unwrap operation failed
+ */
+ WRAP_UNWRAP_FAILED(true),
+
+ /**
+ * KEK operation (create/delete) failed
+ */
+ KEK_OPERATION_FAILED(true),
+
+ /**
+ * Health check failed
+ */
+ HEALTH_CHECK_FAILED(true),
+
+ /**
+ * Transient network or communication error
+ */
+ TRANSIENT_ERROR(true),
+
+ /**
+ * Unknown error
+ */
+ UNKNOWN(false);
+
+ private final boolean retryable;
+
+ ErrorType(boolean retryable) {
+ this.retryable = retryable;
+ }
+
+ public boolean isRetryable() {
+ return retryable;
+ }
+ }
+
+ private final ErrorType errorType;
+
+ public KMSException(String message) {
+ super(message);
+ this.errorType = ErrorType.UNKNOWN;
+ }
+
+ public KMSException(String message, Throwable cause) {
+ super(message, cause);
+ this.errorType = ErrorType.UNKNOWN;
+ }
+
+ public KMSException(ErrorType errorType, String message) {
+ super(message);
+ this.errorType = errorType;
+ }
+
+ public KMSException(ErrorType errorType, String message, Throwable cause) {
+ super(message, cause);
+ this.errorType = errorType;
+ }
+
+ public static KMSException providerNotInitialized(String details) {
+ return new KMSException(ErrorType.PROVIDER_NOT_INITIALIZED,
+ "KMS provider not initialized: " + details);
+ }
+
+ public static KMSException kekNotFound(String kekId) {
+ return new KMSException(ErrorType.KEK_NOT_FOUND,
+ "KEK not found: " + kekId);
+ }
+
+ // Static factory methods for common error types
+
+ public static KMSException keyAlreadyExists(String details) {
+ return new KMSException(ErrorType.KEY_ALREADY_EXISTS,
+ "Key already exists: " + details);
+ }
+
+ public static KMSException invalidParameter(String details) {
+ return new KMSException(ErrorType.INVALID_PARAMETER,
+ "Invalid parameter: " + details);
+ }
+
+ public static KMSException wrapUnwrapFailed(String details, Throwable cause) {
+ return new KMSException(ErrorType.WRAP_UNWRAP_FAILED,
+ "Wrap/unwrap operation failed: " + details, cause);
+ }
+
+ public static KMSException wrapUnwrapFailed(String details) {
+ return new KMSException(ErrorType.WRAP_UNWRAP_FAILED,
+ "Wrap/unwrap operation failed: " + details);
+ }
+
+ public static KMSException kekOperationFailed(String details, Throwable cause) {
+ return new KMSException(ErrorType.KEK_OPERATION_FAILED,
+ "KEK operation failed: " + details, cause);
+ }
+
+ public static KMSException kekOperationFailed(String details) {
+ return new KMSException(ErrorType.KEK_OPERATION_FAILED,
+ "KEK operation failed: " + details);
+ }
+
+ public static KMSException healthCheckFailed(String details, Throwable cause) {
+ return new KMSException(ErrorType.HEALTH_CHECK_FAILED,
+ "Health check failed: " + details, cause);
+ }
+
+ public static KMSException transientError(String details, Throwable cause) {
+ return new KMSException(ErrorType.TRANSIENT_ERROR,
+ "Transient error: " + details, cause);
+ }
+
+ public ErrorType getErrorType() {
+ return errorType;
+ }
+
+ @Override
+ public String toString() {
+ return "KMSException{" +
+ "errorType=" + errorType +
+ ", retryable=" + isRetryable() +
+ ", message='" + getMessage() + '\'' +
+ '}';
+ }
+
+ public boolean isRetryable() {
+ return errorType.isRetryable();
+ }
+}
+
diff --git a/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSProvider.java b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSProvider.java
new file mode 100644
index 00000000000..cfee06f6278
--- /dev/null
+++ b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSProvider.java
@@ -0,0 +1,144 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.framework.kms;
+
+import org.apache.cloudstack.framework.config.Configurable;
+
+import java.util.List;
+
+/**
+ * Abstract provider contract for Key Management Service operations.
+ *
+ * Implementations provide the cryptographic backend (HSM via PKCS#11, database, cloud KMS, etc.)
+ * for secure key wrapping/unwrapping using envelope encryption.
+ *
+ * Design principles:
+ * - KEKs (Key Encryption Keys) never leave the secure backend
+ * - DEKs (Data Encryption Keys) are wrapped by KEKs for storage
+ * - Plaintext DEKs exist only transiently in memory during wrap/unwrap
+ * - All operations are purpose-scoped to prevent key reuse
+ *
+ * Thread-safety: Implementations must be thread-safe for concurrent operations.
+ */
+public interface KMSProvider extends Configurable {
+
+ /**
+ * Get the unique name of this provider
+ *
+ * @return provider name (e.g., "database", "pkcs11")
+ */
+ String getProviderName();
+
+ // ==================== KEK Management ====================
+
+ /**
+ * Create a new Key Encryption Key (KEK) in the secure backend
+ *
+ * @param purpose the purpose/scope for this KEK
+ * @param label human-readable label for the KEK (must be unique within purpose)
+ * @param keyBits key size in bits (typically 128, 192, or 256)
+ * @return the KEK identifier (label or handle) for later reference
+ * @throws KMSException if KEK creation fails
+ */
+ String createKek(KeyPurpose purpose, String label, int keyBits) throws KMSException;
+
+ /**
+ * Delete a KEK from the secure backend.
+ * WARNING: This will make all DEKs wrapped by this KEK unrecoverable.
+ *
+ * @param kekId the KEK identifier to delete
+ * @throws KMSException if deletion fails or KEK not found
+ */
+ void deleteKek(String kekId) throws KMSException;
+
+ /**
+ * List all KEK identifiers for a given purpose
+ *
+ * @param purpose the key purpose to filter by (null = all purposes)
+ * @return list of KEK identifiers
+ * @throws KMSException if listing fails
+ */
+ List listKeks(KeyPurpose purpose) throws KMSException;
+
+ /**
+ * Check if a KEK exists and is accessible
+ *
+ * @param kekId the KEK identifier to check
+ * @return true if KEK is available
+ * @throws KMSException if check fails
+ */
+ boolean isKekAvailable(String kekId) throws KMSException;
+
+ // ==================== DEK Operations ====================
+
+ /**
+ * Wrap (encrypt) a plaintext Data Encryption Key with a KEK
+ *
+ * @param plainDek the plaintext DEK to wrap (caller must zeroize after call)
+ * @param purpose the intended purpose of this DEK
+ * @param kekLabel the label of the KEK to use for wrapping
+ * @return WrappedKey containing the encrypted DEK and metadata
+ * @throws KMSException if wrapping fails or KEK not found
+ */
+ WrappedKey wrapKey(byte[] plainDek, KeyPurpose purpose, String kekLabel) throws KMSException;
+
+ /**
+ * Unwrap (decrypt) a wrapped DEK to obtain the plaintext key
+ *
+ * SECURITY: Caller MUST zeroize the returned byte array after use
+ *
+ * @param wrappedKey the wrapped key to decrypt
+ * @return plaintext DEK (caller must zeroize!)
+ * @throws KMSException if unwrapping fails or KEK not found
+ */
+ byte[] unwrapKey(WrappedKey wrappedKey) throws KMSException;
+
+ /**
+ * Generate a new random DEK and immediately wrap it with a KEK
+ * (convenience method combining generation + wrapping)
+ *
+ * @param purpose the intended purpose of the new DEK
+ * @param kekLabel the label of the KEK to use for wrapping
+ * @param keyBits DEK size in bits (typically 128, 192, or 256)
+ * @return WrappedKey containing the newly generated and wrapped DEK
+ * @throws KMSException if generation or wrapping fails
+ */
+ WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits) throws KMSException;
+
+ /**
+ * Rewrap a DEK with a different KEK (used during key rotation).
+ * This unwraps with the old KEK and wraps with the new KEK without exposing the plaintext DEK.
+ *
+ * @param oldWrappedKey the currently wrapped key
+ * @param newKekLabel the label of the new KEK to wrap with
+ * @return new WrappedKey encrypted with the new KEK
+ * @throws KMSException if rewrapping fails
+ */
+ WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel) throws KMSException;
+
+ // ==================== Health & Status ====================
+
+ /**
+ * Perform health check on the provider backend
+ *
+ * @return true if provider is healthy and operational
+ * @throws KMSException if health check fails with critical error
+ */
+ boolean healthCheck() throws KMSException;
+}
+
diff --git a/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSService.java b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSService.java
new file mode 100644
index 00000000000..d9dc14ea8ca
--- /dev/null
+++ b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSService.java
@@ -0,0 +1,166 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.framework.kms;
+
+import java.util.List;
+
+/**
+ * High-level service interface for Key Management Service operations.
+ *
+ * This facade abstracts provider-specific details and provides zone-aware
+ * routing, retry logic, and audit logging for KMS operations.
+ *
+ * The service handles:
+ * - Zone-scoped provider selection
+ * - Configuration management (which provider, which KEK)
+ * - Retry logic for transient failures
+ * - Audit event emission
+ * - Health monitoring
+ */
+public interface KMSService {
+
+ /**
+ * Get the service name
+ *
+ * @return service name
+ */
+ String getName();
+
+ // ==================== Provider Management ====================
+
+ /**
+ * List all registered KMS providers
+ *
+ * @return list of available providers
+ */
+ List extends KMSProvider> listProviders();
+
+ /**
+ * Get a specific provider by name
+ *
+ * @param name provider name
+ * @return the provider, or null if not found
+ */
+ KMSProvider getProvider(String name);
+
+ /**
+ * Get the configured provider for a specific zone.
+ * Falls back to global default if zone has no specific configuration.
+ *
+ * @param zoneId the zone ID (null for global)
+ * @return the configured provider for the zone
+ * @throws KMSException if no provider configured or provider not found
+ */
+ KMSProvider getProviderForZone(Long zoneId) throws KMSException;
+
+ // ==================== KEK Management ====================
+
+ /**
+ * Create a new KEK for a specific zone and purpose
+ *
+ * @param zoneId the zone ID (null for global)
+ * @param purpose the purpose of the KEK
+ * @param label optional custom label (null for auto-generated)
+ * @param keyBits key size in bits
+ * @return the KEK identifier
+ * @throws KMSException if creation fails
+ */
+ String createKek(Long zoneId, KeyPurpose purpose, String label, int keyBits) throws KMSException;
+
+ /**
+ * Delete a KEK (use with extreme caution!)
+ *
+ * @param zoneId the zone ID
+ * @param kekId the KEK identifier to delete
+ * @throws KMSException if deletion fails
+ */
+ void deleteKek(Long zoneId, String kekId) throws KMSException;
+
+ /**
+ * List KEKs for a zone and purpose
+ *
+ * @param zoneId the zone ID (null for all zones)
+ * @param purpose the purpose filter (null for all purposes)
+ * @return list of KEK identifiers
+ * @throws KMSException if listing fails
+ */
+ List listKeks(Long zoneId, KeyPurpose purpose) throws KMSException;
+
+ /**
+ * Check if a KEK is available in a zone
+ *
+ * @param zoneId the zone ID
+ * @param kekId the KEK identifier
+ * @return true if available
+ * @throws KMSException if check fails
+ */
+ boolean isKekAvailable(Long zoneId, String kekId) throws KMSException;
+
+ /**
+ * Rotate a KEK by creating a new one and rewrapping all associated DEKs.
+ * This is an async operation that may take time for large deployments.
+ *
+ * @param zoneId the zone ID
+ * @param purpose the purpose of keys to rotate
+ * @param oldKekLabel the current KEK label (null for configured default)
+ * @param newKekLabel the new KEK label (null for auto-generated)
+ * @param keyBits the new KEK size in bits
+ * @return the new KEK identifier
+ * @throws KMSException if rotation fails
+ */
+ String rotateKek(Long zoneId, KeyPurpose purpose, String oldKekLabel,
+ String newKekLabel, int keyBits) throws KMSException;
+
+ // ==================== DEK Operations ====================
+
+ /**
+ * Generate and wrap a new DEK for volume encryption
+ *
+ * @param zoneId the zone ID where the volume resides
+ * @param purpose the key purpose (typically VOLUME_ENCRYPTION)
+ * @param kekLabel the KEK label to use (null for configured default)
+ * @param keyBits DEK size in bits
+ * @return wrapped key ready for database storage
+ * @throws KMSException if operation fails
+ */
+ WrappedKey generateAndWrapDek(Long zoneId, KeyPurpose purpose,
+ String kekLabel, int keyBits) throws KMSException;
+
+ /**
+ * Unwrap a DEK for use (e.g., attaching encrypted volume)
+ *
+ * SECURITY: Caller must zeroize the returned byte array after use
+ *
+ * @param wrappedKey the wrapped key from database
+ * @return plaintext DEK (caller must zeroize!)
+ * @throws KMSException if unwrap fails
+ */
+ byte[] unwrapDek(WrappedKey wrappedKey) throws KMSException;
+
+ // ==================== Health & Status ====================
+
+ /**
+ * Check health of KMS provider for a zone
+ *
+ * @param zoneId the zone ID (null for global check)
+ * @return true if healthy
+ * @throws KMSException if health check fails critically
+ */
+ boolean healthCheck(Long zoneId) throws KMSException;
+}
+
diff --git a/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KeyPurpose.java b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KeyPurpose.java
new file mode 100644
index 00000000000..7cbd544f4c7
--- /dev/null
+++ b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KeyPurpose.java
@@ -0,0 +1,82 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.framework.kms;
+
+/**
+ * Defines the purpose/usage scope for cryptographic keys in the KMS system.
+ * This enables proper key segregation and prevents key reuse across different contexts.
+ */
+public enum KeyPurpose {
+ /**
+ * Keys used for encrypting VM disk volumes (LUKS, encrypted storage)
+ */
+ VOLUME_ENCRYPTION("volume", "Volume disk encryption keys"),
+
+ /**
+ * Keys used for protecting TLS certificate private keys
+ */
+ TLS_CERT("tls", "TLS certificate private keys"),
+
+ /**
+ * Keys used for encrypting configuration secrets and sensitive settings
+ */
+ CONFIG_SECRET("config", "Configuration secrets");
+
+ private final String name;
+ private final String description;
+
+ KeyPurpose(String name, String description) {
+ this.name = name;
+ this.description = description;
+ }
+
+ /**
+ * Convert string name to KeyPurpose enum
+ *
+ * @param name the string representation of the purpose
+ * @return matching KeyPurpose
+ * @throws IllegalArgumentException if no matching purpose found
+ */
+ public static KeyPurpose fromString(String name) {
+ for (KeyPurpose purpose : KeyPurpose.values()) {
+ if (purpose.getName().equalsIgnoreCase(name)) {
+ return purpose;
+ }
+ }
+ throw new IllegalArgumentException("Unknown KeyPurpose: " + name);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Generate a KEK label with purpose prefix
+ *
+ * @param customLabel optional custom label suffix
+ * @return formatted KEK label (e.g., "volume-kek-v1")
+ */
+ public String generateKekLabel(String customLabel) {
+ return name + "-kek-" + (customLabel != null ? customLabel : "v1");
+ }
+}
+
diff --git a/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/WrappedKey.java b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/WrappedKey.java
new file mode 100644
index 00000000000..fccf45119e7
--- /dev/null
+++ b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/WrappedKey.java
@@ -0,0 +1,165 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.framework.kms;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * Immutable Data Transfer Object representing an encrypted (wrapped) Data Encryption Key.
+ * The wrapped key material contains the DEK encrypted by a Key Encryption Key (KEK)
+ * stored in a secure backend (HSM, database, etc.).
+ *
+ * This follows the envelope encryption pattern:
+ * - DEK: encrypts actual data (e.g., disk volume)
+ * - KEK: encrypts the DEK (never leaves secure storage)
+ * - Wrapped Key: DEK encrypted by KEK, safe to store in database
+ */
+public class WrappedKey {
+ private final String id;
+ private final String kekId;
+ private final KeyPurpose purpose;
+ private final String algorithm;
+ private final byte[] wrappedKeyMaterial;
+ private final String providerName;
+ private final Date created;
+ private final Long zoneId;
+
+ /**
+ * Create a new WrappedKey instance
+ *
+ * @param kekId ID/label of the KEK used to wrap this key
+ * @param purpose the intended use of this key
+ * @param algorithm encryption algorithm (e.g., "AES/GCM/NoPadding")
+ * @param wrappedKeyMaterial the encrypted DEK blob
+ * @param providerName name of the KMS provider that created this key
+ * @param created timestamp when key was wrapped
+ * @param zoneId optional zone ID for zone-scoped keys
+ */
+ public WrappedKey(String kekId, KeyPurpose purpose, String algorithm,
+ byte[] wrappedKeyMaterial, String providerName,
+ Date created, Long zoneId) {
+ this.id = null; // Will be set when persisted to DB
+ this.kekId = Objects.requireNonNull(kekId, "kekId cannot be null");
+ this.purpose = Objects.requireNonNull(purpose, "purpose cannot be null");
+ this.algorithm = Objects.requireNonNull(algorithm, "algorithm cannot be null");
+ this.providerName = providerName;
+
+ // Defensive copy to prevent external modification
+ if (wrappedKeyMaterial == null || wrappedKeyMaterial.length == 0) {
+ throw new IllegalArgumentException("wrappedKeyMaterial cannot be null or empty");
+ }
+ this.wrappedKeyMaterial = Arrays.copyOf(wrappedKeyMaterial, wrappedKeyMaterial.length);
+
+ this.created = created != null ? new Date(created.getTime()) : new Date();
+ this.zoneId = zoneId;
+ }
+
+ /**
+ * Constructor for database-loaded keys with ID
+ */
+ public WrappedKey(String id, String kekId, KeyPurpose purpose, String algorithm,
+ byte[] wrappedKeyMaterial, String providerName,
+ Date created, Long zoneId) {
+ this.id = id;
+ this.kekId = Objects.requireNonNull(kekId, "kekId cannot be null");
+ this.purpose = Objects.requireNonNull(purpose, "purpose cannot be null");
+ this.algorithm = Objects.requireNonNull(algorithm, "algorithm cannot be null");
+ this.providerName = providerName;
+
+ if (wrappedKeyMaterial == null || wrappedKeyMaterial.length == 0) {
+ throw new IllegalArgumentException("wrappedKeyMaterial cannot be null or empty");
+ }
+ this.wrappedKeyMaterial = Arrays.copyOf(wrappedKeyMaterial, wrappedKeyMaterial.length);
+
+ this.created = created != null ? new Date(created.getTime()) : new Date();
+ this.zoneId = zoneId;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getKekId() {
+ return kekId;
+ }
+
+ public KeyPurpose getPurpose() {
+ return purpose;
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ /**
+ * Get wrapped key material. Returns a defensive copy to prevent modification.
+ * Caller is responsible for zeroizing the returned array after use.
+ */
+ public byte[] getWrappedKeyMaterial() {
+ return Arrays.copyOf(wrappedKeyMaterial, wrappedKeyMaterial.length);
+ }
+
+ public String getProviderName() {
+ return providerName;
+ }
+
+ public Date getCreated() {
+ return created != null ? new Date(created.getTime()) : null;
+ }
+
+ public Long getZoneId() {
+ return zoneId;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(id, kekId, purpose, algorithm, providerName);
+ result = 31 * result + Arrays.hashCode(wrappedKeyMaterial);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ WrappedKey that = (WrappedKey) o;
+ return Objects.equals(id, that.id) &&
+ Objects.equals(kekId, that.kekId) &&
+ purpose == that.purpose &&
+ Objects.equals(algorithm, that.algorithm) &&
+ Arrays.equals(wrappedKeyMaterial, that.wrappedKeyMaterial) &&
+ Objects.equals(providerName, that.providerName);
+ }
+
+ @Override
+ public String toString() {
+ return "WrappedKey{" +
+ "id='" + id + '\'' +
+ ", kekId='" + kekId + '\'' +
+ ", purpose=" + purpose +
+ ", algorithm='" + algorithm + '\'' +
+ ", providerName='" + providerName + '\'' +
+ ", materialLength=" + (wrappedKeyMaterial != null ? wrappedKeyMaterial.length : 0) +
+ ", created=" + created +
+ ", zoneId=" + zoneId +
+ '}';
+ }
+}
+
diff --git a/framework/pom.xml b/framework/pom.xml
index 337e5b0268b..95d0bd0694c 100644
--- a/framework/pom.xml
+++ b/framework/pom.xml
@@ -54,6 +54,7 @@
extensions
ipc
jobs
+ kms
managed-context
quota
rest
diff --git a/plugins/kms/database/pom.xml b/plugins/kms/database/pom.xml
new file mode 100644
index 00000000000..1a2c9271d02
--- /dev/null
+++ b/plugins/kms/database/pom.xml
@@ -0,0 +1,73 @@
+
+
+
+ 4.0.0
+ cloud-plugin-kms-database
+ Apache CloudStack Plugin - KMS Database Provider
+ Database-backed KMS provider for encrypted key storage
+
+
+ org.apache.cloudstack
+ cloudstack-kms-plugins
+ 4.23.0.0-SNAPSHOT
+ ../pom.xml
+
+
+
+
+ org.apache.cloudstack
+ cloud-framework-kms
+ ${project.version}
+
+
+ org.apache.cloudstack
+ cloud-framework-config
+ ${project.version}
+
+
+ org.apache.cloudstack
+ cloud-utils
+ ${project.version}
+
+
+ com.google.crypto.tink
+ tink
+ ${cs.tink.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ true
+
+
+
+
+
+
+
diff --git a/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/DatabaseKMSProvider.java b/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/DatabaseKMSProvider.java
new file mode 100644
index 00000000000..aab86660657
--- /dev/null
+++ b/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/DatabaseKMSProvider.java
@@ -0,0 +1,390 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.cloudstack.kms.provider;
+
+import com.google.crypto.tink.subtle.AesGcmJce;
+import org.apache.cloudstack.framework.config.ConfigKey;
+import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
+import org.apache.cloudstack.framework.config.impl.ConfigurationVO;
+import org.apache.cloudstack.framework.kms.KMSException;
+import org.apache.cloudstack.framework.kms.KMSProvider;
+import org.apache.cloudstack.framework.kms.KeyPurpose;
+import org.apache.cloudstack.framework.kms.WrappedKey;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.inject.Inject;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Database-backed KMS provider that stores master KEKs encrypted in the configuration table.
+ * Uses AES-256-GCM for all cryptographic operations.
+ *
+ * This provider is suitable for deployments that don't have access to HSM hardware.
+ * The master KEKs are stored encrypted using CloudStack's existing DBEncryptionUtil.
+ */
+public class DatabaseKMSProvider implements KMSProvider {
+ // Configuration keys
+ public static final ConfigKey CacheEnabled = new ConfigKey<>(
+ "Advanced",
+ Boolean.class,
+ "kms.database.cache.enabled",
+ "true",
+ "Enable in-memory caching of KEKs for better performance",
+ true,
+ ConfigKey.Scope.Global
+ );
+ private static final Logger logger = LogManager.getLogger(DatabaseKMSProvider.class);
+ private static final String PROVIDER_NAME = "database";
+ private static final String KEK_CONFIG_PREFIX = "kms.database.kek.";
+ private static final int GCM_IV_LENGTH = 12; // 96 bits recommended for GCM
+ private static final int GCM_TAG_LENGTH = 16; // 128 bits
+ private static final String ALGORITHM = "AES/GCM/NoPadding";
+ // In-memory cache of KEKs (encrypted form cached, decrypted on demand)
+ private final Map kekCache = new ConcurrentHashMap<>();
+ private final SecureRandom secureRandom = new SecureRandom();
+ @Inject
+ private ConfigurationDao configDao;
+
+ @Override
+ public String getProviderName() {
+ return PROVIDER_NAME;
+ }
+
+ @Override
+ public String createKek(KeyPurpose purpose, String label, int keyBits) throws KMSException {
+ if (keyBits != 128 && keyBits != 192 && keyBits != 256) {
+ throw KMSException.invalidParameter("Key size must be 128, 192, or 256 bits");
+ }
+
+ if (StringUtils.isEmpty(label)) {
+ label = generateKekLabel(purpose);
+ }
+
+ String configKey = buildConfigKey(label);
+
+ // Check if KEK already exists
+ ConfigurationVO existing = configDao.findByName(configKey);
+ if (existing != null) {
+ throw KMSException.keyAlreadyExists("KEK with label " + label + " already exists");
+ }
+
+ try {
+ // Generate random KEK
+ byte[] kekBytes = new byte[keyBits / 8];
+ secureRandom.nextBytes(kekBytes);
+
+ // Store in configuration table (will be encrypted automatically due to "Secure" category)
+ String kekBase64 = java.util.Base64.getEncoder().encodeToString(kekBytes);
+ ConfigurationVO config = new ConfigurationVO(
+ "Secure", // Category - triggers encryption
+ "DEFAULT",
+ getConfigComponentName(),
+ configKey,
+ kekBase64,
+ "KMS KEK for " + purpose.getName() + " (label: " + label + ")"
+ );
+ configDao.persist(config);
+
+ // Cache the KEK
+ if (CacheEnabled.value()) {
+ kekCache.put(label, kekBytes);
+ }
+
+ logger.info("Created KEK with label {} for purpose {}", label, purpose);
+ return label;
+
+ } catch (Exception e) {
+ throw KMSException.kekOperationFailed("Failed to create KEK: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public String getConfigComponentName() {
+ return DatabaseKMSProvider.class.getSimpleName();
+ }
+
+ @Override
+ public ConfigKey>[] getConfigKeys() {
+ return new ConfigKey>[]{
+ CacheEnabled
+ };
+ }
+
+ @Override
+ public void deleteKek(String kekId) throws KMSException {
+ String configKey = buildConfigKey(kekId);
+
+ ConfigurationVO config = configDao.findByName(configKey);
+ if (config == null) {
+ throw KMSException.kekNotFound("KEK with label " + kekId + " not found");
+ }
+
+ try {
+ // Remove from configuration (name is the primary key)
+ configDao.remove(config.getName());
+
+ // Remove from cache
+ byte[] cachedKek = kekCache.remove(kekId);
+ if (cachedKek != null) {
+ Arrays.fill(cachedKek, (byte) 0); // Zeroize
+ }
+
+ logger.warn("Deleted KEK with label {}. All DEKs wrapped with this KEK are now unrecoverable!", kekId);
+
+ } catch (Exception e) {
+ throw KMSException.kekOperationFailed("Failed to delete KEK: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public List listKeks(KeyPurpose purpose) throws KMSException {
+ try {
+ List keks = new ArrayList<>();
+
+ // We can't efficiently list all KEKs without a custom query
+ // For now, return cached keys only - KEKs will be tracked via cache
+ // TODO: Add custom DAO method or maintain KEK registry
+ logger.debug("listKeks called for purpose: {}. Returning cached keys only.", purpose);
+
+ // Return keys from cache
+ for (String label : kekCache.keySet()) {
+ if (purpose == null || label.startsWith(purpose.getName())) {
+ keks.add(label);
+ }
+ }
+
+ return keks;
+ } catch (Exception e) {
+ throw KMSException.kekOperationFailed("Failed to list KEKs: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public boolean isKekAvailable(String kekId) throws KMSException {
+ try {
+ String configKey = buildConfigKey(kekId);
+ ConfigurationVO config = configDao.findByName(configKey);
+ return config != null && config.getValue() != null;
+ } catch (Exception e) {
+ logger.warn("Error checking KEK availability: {}", e.getMessage());
+ return false;
+ }
+ }
+
+ @Override
+ public WrappedKey wrapKey(byte[] plainKey, KeyPurpose purpose, String kekLabel) throws KMSException {
+ if (plainKey == null || plainKey.length == 0) {
+ throw KMSException.invalidParameter("Plain key cannot be null or empty");
+ }
+
+ byte[] kekBytes = loadKek(kekLabel);
+
+ try {
+ // Create AES-GCM cipher with the KEK
+ // Tink's AesGcmJce automatically generates a random IV and prepends it to the ciphertext
+ AesGcmJce aesgcm = new AesGcmJce(kekBytes);
+
+ // Encrypt the DEK (Tink's encrypt returns [IV][ciphertext+tag] format)
+ byte[] wrappedBlob = aesgcm.encrypt(plainKey, new byte[0]); // Empty associated data
+
+ WrappedKey wrapped = new WrappedKey(
+ kekLabel,
+ purpose,
+ ALGORITHM,
+ wrappedBlob,
+ PROVIDER_NAME,
+ new Date(),
+ null // zoneId set by caller
+ );
+
+ logger.debug("Wrapped {} key with KEK {}", purpose, kekLabel);
+ return wrapped;
+
+ } catch (Exception e) {
+ throw KMSException.wrapUnwrapFailed("Failed to wrap key: " + e.getMessage(), e);
+ } finally {
+ // Zeroize KEK
+ Arrays.fill(kekBytes, (byte) 0);
+ }
+ }
+
+ @Override
+ public byte[] unwrapKey(WrappedKey wrappedKey) throws KMSException {
+ if (wrappedKey == null) {
+ throw KMSException.invalidParameter("Wrapped key cannot be null");
+ }
+
+ byte[] kekBytes = loadKek(wrappedKey.getKekId());
+
+ try {
+ // Create AES-GCM cipher with the KEK
+ AesGcmJce aesgcm = new AesGcmJce(kekBytes);
+
+ // Tink's decrypt expects [IV][ciphertext+tag] format (same as encrypt returns)
+ byte[] blob = wrappedKey.getWrappedKeyMaterial();
+ if (blob.length < GCM_IV_LENGTH + GCM_TAG_LENGTH) {
+ throw new KMSException(KMSException.ErrorType.WRAP_UNWRAP_FAILED,
+ "Invalid wrapped key format: too short");
+ }
+
+ // Decrypt the DEK (Tink extracts IV from the blob automatically)
+ byte[] plainKey = aesgcm.decrypt(blob, new byte[0]); // Empty associated data
+
+ logger.debug("Unwrapped {} key with KEK {}", wrappedKey.getPurpose(), wrappedKey.getKekId());
+ return plainKey;
+
+ } catch (KMSException e) {
+ throw e;
+ } catch (Exception e) {
+ throw KMSException.wrapUnwrapFailed("Failed to unwrap key: " + e.getMessage(), e);
+ } finally {
+ // Zeroize KEK
+ Arrays.fill(kekBytes, (byte) 0);
+ }
+ }
+
+ @Override
+ public WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits) throws KMSException {
+ if (keyBits != 128 && keyBits != 192 && keyBits != 256) {
+ throw KMSException.invalidParameter("DEK size must be 128, 192, or 256 bits");
+ }
+
+ // Generate random DEK
+ byte[] dekBytes = new byte[keyBits / 8];
+ secureRandom.nextBytes(dekBytes);
+
+ try {
+ return wrapKey(dekBytes, purpose, kekLabel);
+ } finally {
+ // Zeroize DEK (wrapped version is in WrappedKey)
+ Arrays.fill(dekBytes, (byte) 0);
+ }
+ }
+
+ @Override
+ public WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel) throws KMSException {
+ // Unwrap with old KEK
+ byte[] plainKey = unwrapKey(oldWrappedKey);
+
+ try {
+ // Wrap with new KEK
+ return wrapKey(plainKey, oldWrappedKey.getPurpose(), newKekLabel);
+ } finally {
+ // Zeroize plaintext DEK
+ Arrays.fill(plainKey, (byte) 0);
+ }
+ }
+
+ @Override
+ public boolean healthCheck() throws KMSException {
+ try {
+ // Verify we can access configuration
+ if (configDao == null) {
+ logger.error("Configuration DAO is not initialized");
+ return false;
+ }
+
+ // Try to list KEKs (lightweight operation)
+ List keks = listKeks(null);
+ logger.debug("Health check passed. Found {} KEKs", keks.size());
+
+ // Optionally verify we can perform wrap/unwrap
+ byte[] testKey = new byte[32];
+ secureRandom.nextBytes(testKey);
+
+ // If we have any KEK, test it
+ if (!keks.isEmpty()) {
+ String testKek = keks.get(0);
+ WrappedKey wrapped = wrapKey(testKey, KeyPurpose.VOLUME_ENCRYPTION, testKek);
+ byte[] unwrapped = unwrapKey(wrapped);
+
+ boolean matches = Arrays.equals(testKey, unwrapped);
+ Arrays.fill(unwrapped, (byte) 0);
+
+ if (!matches) {
+ logger.error("Health check failed: wrap/unwrap test failed");
+ return false;
+ }
+ }
+
+ Arrays.fill(testKey, (byte) 0);
+ return true;
+
+ } catch (Exception e) {
+ throw KMSException.healthCheckFailed("Health check failed: " + e.getMessage(), e);
+ }
+ }
+
+ // ==================== Private Helper Methods ====================
+
+ private byte[] loadKek(String kekLabel) throws KMSException {
+ // Check cache first
+ if (CacheEnabled.value()) {
+ byte[] cached = kekCache.get(kekLabel);
+ if (cached != null) {
+ return Arrays.copyOf(cached, cached.length); // Return copy
+ }
+ }
+
+ // Load from database
+ String configKey = buildConfigKey(kekLabel);
+ ConfigurationVO config = configDao.findByName(configKey);
+
+ if (config == null) {
+ throw KMSException.kekNotFound("KEK with label " + kekLabel + " not found");
+ }
+
+ try {
+ // getValue() automatically decrypts
+ String kekBase64 = config.getValue();
+ if (StringUtils.isEmpty(kekBase64)) {
+ throw KMSException.kekNotFound("KEK value is empty for label " + kekLabel);
+ }
+
+ byte[] kekBytes = java.util.Base64.getDecoder().decode(kekBase64);
+
+ // Cache for future use
+ if (CacheEnabled.value()) {
+ kekCache.put(kekLabel, Arrays.copyOf(kekBytes, kekBytes.length));
+ }
+
+ return kekBytes;
+
+ } catch (IllegalArgumentException e) {
+ throw KMSException.kekOperationFailed("Invalid KEK encoding for label " + kekLabel, e);
+ }
+ }
+
+ private String buildConfigKey(String label) {
+ return KEK_CONFIG_PREFIX + label;
+ }
+
+ private String generateKekLabel(KeyPurpose purpose) {
+ return purpose.getName() + "-kek-" + UUID.randomUUID().toString().substring(0, 8);
+ }
+}
+
diff --git a/plugins/kms/database/src/main/resources/META-INF/cloudstack/database-kms/module.properties b/plugins/kms/database/src/main/resources/META-INF/cloudstack/database-kms/module.properties
new file mode 100644
index 00000000000..57d436bcea5
--- /dev/null
+++ b/plugins/kms/database/src/main/resources/META-INF/cloudstack/database-kms/module.properties
@@ -0,0 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name=database-kms
+parent=kmsProvidersRegistry
+
diff --git a/plugins/kms/database/src/main/resources/META-INF/cloudstack/database-kms/spring-database-kms-context.xml b/plugins/kms/database/src/main/resources/META-INF/cloudstack/database-kms/spring-database-kms-context.xml
new file mode 100644
index 00000000000..be2e666a74d
--- /dev/null
+++ b/plugins/kms/database/src/main/resources/META-INF/cloudstack/database-kms/spring-database-kms-context.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
diff --git a/plugins/kms/pom.xml b/plugins/kms/pom.xml
new file mode 100644
index 00000000000..afff4024e96
--- /dev/null
+++ b/plugins/kms/pom.xml
@@ -0,0 +1,39 @@
+
+
+
+ 4.0.0
+ cloudstack-kms-plugins
+ pom
+ Apache CloudStack Plugin - KMS
+ Key Management Service providers
+
+
+ org.apache.cloudstack
+ cloudstack-plugins
+ 4.23.0.0-SNAPSHOT
+ ../pom.xml
+
+
+
+ database
+
+
diff --git a/plugins/pom.xml b/plugins/pom.xml
index e7d13871285..4b4aae9479c 100755
--- a/plugins/pom.xml
+++ b/plugins/pom.xml
@@ -97,6 +97,8 @@
integrations/prometheus
integrations/kubernetes-service
+ kms
+
metrics
network-elements/bigswitch
diff --git a/server/pom.xml b/server/pom.xml
index 2b35a0f42ac..a44c3af0e73 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -69,6 +69,11 @@
cloud-framework-ca
${project.version}