sc = rewrapExcludeVersionSearch.create();
+ sc.setParameters("kmsKeyId", kmsKeyId);
+ sc.setParameters("kekVersionId", excludeKekVersionId);
+ return listBy(sc);
+ }
+}
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 ff5651ba38f..e7dde87c99f 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
@@ -185,7 +185,55 @@ CREATE TABLE IF NOT EXISTS `cloud`.`kms_wrapped_key` (
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 key reference to volumes table (which KMS key was used)
+CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.volumes', 'kms_key_id', 'BIGINT UNSIGNED COMMENT ''KMS key ID used for volume encryption''');
+CALL `cloud`.`IDEMPOTENT_ADD_FOREIGN_KEY`('cloud.volumes', 'fk_volumes__kms_key_id', '(kms_key_id)', '`kms_keys`(`id`)');
+
-- 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`)');
+-- KMS Database Provider KEK Objects (PKCS#11-like object storage)
+-- Stores KEKs for the database KMS provider in a PKCS#11-compatible format
+CREATE TABLE IF NOT EXISTS `cloud`.`kms_database_kek_objects` (
+ `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Object handle (PKCS#11 CKA_HANDLE)',
+ `uuid` VARCHAR(40) NOT NULL COMMENT 'UUID',
+ -- PKCS#11 Object Class (CKA_CLASS)
+ `object_class` VARCHAR(32) NOT NULL DEFAULT 'CKO_SECRET_KEY' COMMENT 'PKCS#11 object class (CKO_SECRET_KEY, CKO_PRIVATE_KEY, etc.)',
+ -- PKCS#11 Label (CKA_LABEL) - human-readable identifier
+ `label` VARCHAR(255) NOT NULL COMMENT 'PKCS#11 label (CKA_LABEL) - human-readable identifier',
+ -- PKCS#11 ID (CKA_ID) - application-defined identifier
+ `object_id` VARBINARY(64) COMMENT 'PKCS#11 object ID (CKA_ID) - application-defined identifier',
+ -- Key Type (CKA_KEY_TYPE)
+ `key_type` VARCHAR(32) NOT NULL DEFAULT 'CKK_AES' COMMENT 'PKCS#11 key type (CKK_AES, CKK_RSA, etc.)',
+ -- Key Material (CKA_VALUE) - encrypted KEK material
+ `key_material` VARBINARY(512) NOT NULL COMMENT 'PKCS#11 key value (CKA_VALUE) - encrypted KEK material',
+ -- Key Attributes (PKCS#11 boolean attributes)
+ `is_sensitive` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'PKCS#11 CKA_SENSITIVE - key material is sensitive',
+ `is_extractable` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'PKCS#11 CKA_EXTRACTABLE - key can be extracted',
+ `is_token` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'PKCS#11 CKA_TOKEN - object is on token (persistent)',
+ `is_private` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'PKCS#11 CKA_PRIVATE - object is private',
+ `is_modifiable` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'PKCS#11 CKA_MODIFIABLE - object can be modified',
+ `is_copyable` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'PKCS#11 CKA_COPYABLE - object can be copied',
+ `is_destroyable` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'PKCS#11 CKA_DESTROYABLE - object can be destroyed',
+ `always_sensitive` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'PKCS#11 CKA_ALWAYS_SENSITIVE - key was always sensitive',
+ `never_extractable` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'PKCS#11 CKA_NEVER_EXTRACTABLE - key was never extractable',
+ -- Key Metadata
+ `purpose` VARCHAR(32) NOT NULL COMMENT 'Key purpose (VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET)',
+ `key_bits` INT NOT NULL COMMENT 'Key size in bits (128, 192, 256)',
+ `algorithm` VARCHAR(64) NOT NULL DEFAULT 'AES/GCM/NoPadding' COMMENT 'Encryption algorithm',
+ -- Validity Dates (PKCS#11 CKA_START_DATE, CKA_END_DATE)
+ `start_date` DATETIME COMMENT 'PKCS#11 CKA_START_DATE - key validity start',
+ `end_date` DATETIME COMMENT 'PKCS#11 CKA_END_DATE - key validity end',
+ -- Lifecycle
+ `created` DATETIME NOT NULL COMMENT 'Creation timestamp',
+ `last_used` DATETIME COMMENT 'Last usage timestamp',
+ `removed` DATETIME COMMENT 'Removal timestamp for soft delete',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_uuid` (`uuid`),
+ UNIQUE KEY `uk_label_removed` (`label`, `removed`),
+ INDEX `idx_purpose_removed` (`purpose`, `removed`),
+ INDEX `idx_key_type` (`key_type`, `removed`),
+ INDEX `idx_object_class` (`object_class`, `removed`),
+ INDEX `idx_created` (`created`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KMS Database Provider KEK Objects - PKCS#11-like object storage for KEKs';
diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java
index 43218b3f6a0..2a7b286aaf2 100644
--- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java
+++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java
@@ -46,6 +46,8 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataObjectInStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
+import org.apache.cloudstack.kms.KMSManager;
+import org.apache.cloudstack.kms.dao.KMSWrappedKeyDao;
import org.apache.cloudstack.storage.command.CopyCmdAnswer;
import org.apache.cloudstack.storage.command.CreateObjectAnswer;
import org.apache.cloudstack.storage.datastore.ObjectInDataStoreManager;
@@ -98,6 +100,10 @@ public class VolumeObject implements VolumeInfo {
@Inject
VolumeDataStoreDao volumeStoreDao;
@Inject
+ KMSManager kmsManager;
+ @Inject
+ KMSWrappedKeyDao kmsWrappedKeyDao;
+ @Inject
ObjectInDataStoreManager objectInStoreMgr;
@Inject
ResourceLimitService resourceLimitMgr;
@@ -900,6 +906,26 @@ public class VolumeObject implements VolumeInfo {
volumeVO.setPassphraseId(id);
}
+ @Override
+ public Long getKmsKeyId() {
+ return volumeVO.getKmsKeyId();
+ }
+
+ @Override
+ public void setKmsKeyId(Long id) {
+ volumeVO.setKmsKeyId(id);
+ }
+
+ @Override
+ public Long getKmsWrappedKeyId() {
+ return volumeVO.getKmsWrappedKeyId();
+ }
+
+ @Override
+ public void setKmsWrappedKeyId(Long id) {
+ volumeVO.setKmsWrappedKeyId(id);
+ }
+
/**
* Removes passphrase reference from underlying volume. Also removes the associated passphrase entry if it is the last user.
*/
@@ -929,9 +955,21 @@ public class VolumeObject implements VolumeInfo {
/**
* Looks up passphrase from underlying volume.
- * @return passphrase as bytes
+ * Supports both legacy passphrase-based encryption and KMS-based encryption.
+ * @return passphrase/DEK as bytes
*/
public byte[] getPassphrase() {
+ // First check for KMS-encrypted volume
+ if (volumeVO.getKmsWrappedKeyId() != null) {
+ try {
+ return kmsManager.unwrapKey(volumeVO.getKmsWrappedKeyId());
+ } catch (org.apache.cloudstack.framework.kms.KMSException e) {
+ logger.error("Failed to unwrap KMS key for volume {}: {}", volumeVO.getId(), e.getMessage());
+ return new byte[0];
+ }
+ }
+
+ // Fallback to legacy passphrase-based encryption
PassphraseVO passphrase = passphraseDao.findById(volumeVO.getPassphraseId());
if (passphrase != null) {
return passphrase.getPassphrase();
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
index 58b8d251a57..59af8f5f6a6 100644
--- 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
@@ -176,4 +176,3 @@ public class KMSException extends CloudRuntimeException {
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
index cfee06f6278..7ab881de1cf 100644
--- 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
@@ -19,6 +19,8 @@ package org.apache.cloudstack.framework.kms;
import org.apache.cloudstack.framework.config.Configurable;
+import com.cloud.utils.component.Adapter;
+
import java.util.List;
/**
@@ -35,7 +37,7 @@ import java.util.List;
*
* Thread-safety: Implementations must be thread-safe for concurrent operations.
*/
-public interface KMSProvider extends Configurable {
+public interface KMSProvider extends Configurable, Adapter {
/**
* Get the unique name of this provider
@@ -141,4 +143,3 @@ public interface KMSProvider extends Configurable {
*/
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
deleted file mode 100644
index d9dc14ea8ca..00000000000
--- a/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSService.java
+++ /dev/null
@@ -1,166 +0,0 @@
-// 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
index 7cbd544f4c7..cea182eb75e 100644
--- 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
@@ -30,12 +30,7 @@ public enum KeyPurpose {
/**
* 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");
+ TLS_CERT("tls", "TLS certificate private keys");
private final String name;
private final String description;
@@ -79,4 +74,3 @@ public enum KeyPurpose {
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
index fccf45119e7..e70c5e32c46 100644
--- 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
@@ -32,7 +32,7 @@ import java.util.Objects;
* - Wrapped Key: DEK encrypted by KEK, safe to store in database
*/
public class WrappedKey {
- private final String id;
+ private final String uuid;
private final String kekId;
private final KeyPurpose purpose;
private final String algorithm;
@@ -55,29 +55,16 @@ public class WrappedKey {
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;
+ this(null, kekId, purpose, algorithm, wrappedKeyMaterial, providerName, created, zoneId);
}
/**
* Constructor for database-loaded keys with ID
*/
- public WrappedKey(String id, String kekId, KeyPurpose purpose, String algorithm,
+ public WrappedKey(String uuid, String kekId, KeyPurpose purpose, String algorithm,
byte[] wrappedKeyMaterial, String providerName,
Date created, Long zoneId) {
- this.id = id;
+ this.uuid = uuid;
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");
@@ -92,8 +79,8 @@ public class WrappedKey {
this.zoneId = zoneId;
}
- public String getId() {
- return id;
+ public String getUuid() {
+ return uuid;
}
public String getKekId() {
@@ -128,30 +115,10 @@ public class WrappedKey {
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 + '\'' +
+ "uuid='" + uuid + '\'' +
", kekId='" + kekId + '\'' +
", purpose=" + purpose +
", algorithm='" + algorithm + '\'' +
@@ -162,4 +129,3 @@ public class WrappedKey {
'}';
}
}
-
diff --git a/plugins/kms/database/pom.xml b/plugins/kms/database/pom.xml
index 1a2c9271d02..2bbeb2dc75b 100644
--- a/plugins/kms/database/pom.xml
+++ b/plugins/kms/database/pom.xml
@@ -17,8 +17,8 @@
specific language governing permissions and limitations
under the License.
-->
-
4.0.0
cloud-plugin-kms-database
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
index aab86660657..30736a59456 100644
--- 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
@@ -17,22 +17,26 @@
package org.apache.cloudstack.kms.provider;
+import com.cloud.utils.component.AdapterBase;
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.cloudstack.kms.provider.database.KMSDatabaseKekObjectVO;
+import org.apache.cloudstack.kms.provider.database.dao.KMSDatabaseKekObjectDao;
+import com.cloud.utils.crypt.DBEncryptionUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
+import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Base64;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -40,13 +44,14 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
- * Database-backed KMS provider that stores master KEKs encrypted in the configuration table.
+ * Database-backed KMS provider that stores master KEKs in a PKCS#11-like object 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.
+ * The master KEKs are stored encrypted in the kms_database_kek_objects table using
+ * CloudStack's existing DBEncryptionUtil, with PKCS#11-compatible attributes.
*/
-public class DatabaseKMSProvider implements KMSProvider {
+public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
// Configuration keys
public static final ConfigKey CacheEnabled = new ConfigKey<>(
"Advanced",
@@ -59,15 +64,17 @@ public class DatabaseKMSProvider implements KMSProvider {
);
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";
+ // PKCS#11 constants
+ private static final String CKO_SECRET_KEY = "CKO_SECRET_KEY";
+ private static final String CKK_AES = "CKK_AES";
// 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;
+ private KMSDatabaseKekObjectDao kekObjectDao;
@Override
public String getProviderName() {
@@ -84,11 +91,8 @@ public class DatabaseKMSProvider implements KMSProvider {
label = generateKekLabel(purpose);
}
- String configKey = buildConfigKey(label);
-
// Check if KEK already exists
- ConfigurationVO existing = configDao.findByName(configKey);
- if (existing != null) {
+ if (kekObjectDao.existsByLabel(label)) {
throw KMSException.keyAlreadyExists("KEK with label " + label + " already exists");
}
@@ -97,24 +101,36 @@ public class DatabaseKMSProvider implements KMSProvider {
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);
+ // Encrypt the KEK material using DBEncryptionUtil (Base64 encode first, then encrypt)
+ String kekBase64 = Base64.getEncoder().encodeToString(kekBytes);
+ String encryptedKek = DBEncryptionUtil.encrypt(kekBase64);
+ byte[] encryptedKekBytes = encryptedKek.getBytes(StandardCharsets.UTF_8);
+
+ // Create PKCS#11-like object
+ KMSDatabaseKekObjectVO kekObject = new KMSDatabaseKekObjectVO(label, purpose, keyBits, encryptedKekBytes);
+ kekObject.setObjectClass(CKO_SECRET_KEY);
+ kekObject.setKeyType(CKK_AES);
+ kekObject.setObjectId(label.getBytes(StandardCharsets.UTF_8));
+ kekObject.setAlgorithm(ALGORITHM);
+ // PKCS#11 attributes for KEK
+ kekObject.setIsSensitive(true);
+ kekObject.setIsExtractable(false);
+ kekObject.setIsToken(true);
+ kekObject.setIsPrivate(true);
+ kekObject.setIsModifiable(false);
+ kekObject.setIsCopyable(false);
+ kekObject.setIsDestroyable(true);
+ kekObject.setAlwaysSensitive(true);
+ kekObject.setNeverExtractable(true);
+
+ kekObjectDao.persist(kekObject);
// Cache the KEK
if (CacheEnabled.value()) {
kekCache.put(label, kekBytes);
}
- logger.info("Created KEK with label {} for purpose {}", label, purpose);
+ logger.info("Created KEK with label {} for purpose {} (PKCS#11 object ID: {})", label, purpose, kekObject.getId());
return label;
} catch (Exception e) {
@@ -136,16 +152,13 @@ public class DatabaseKMSProvider implements KMSProvider {
@Override
public void deleteKek(String kekId) throws KMSException {
- String configKey = buildConfigKey(kekId);
-
- ConfigurationVO config = configDao.findByName(configKey);
- if (config == null) {
+ KMSDatabaseKekObjectVO kekObject = kekObjectDao.findByLabel(kekId);
+ if (kekObject == null) {
throw KMSException.kekNotFound("KEK with label " + kekId + " not found");
}
try {
- // Remove from configuration (name is the primary key)
- configDao.remove(config.getName());
+ kekObjectDao.remove(kekObject.getId());
// Remove from cache
byte[] cachedKek = kekCache.remove(kekId);
@@ -153,8 +166,12 @@ public class DatabaseKMSProvider implements KMSProvider {
Arrays.fill(cachedKek, (byte) 0); // Zeroize
}
- logger.warn("Deleted KEK with label {}. All DEKs wrapped with this KEK are now unrecoverable!", kekId);
+ // Zeroize key material in database object
+ if (kekObject.getKeyMaterial() != null) {
+ Arrays.fill(kekObject.getKeyMaterial(), (byte) 0);
+ }
+ 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);
}
@@ -165,18 +182,20 @@ public class DatabaseKMSProvider implements KMSProvider {
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);
+ List kekObjects;
+ if (purpose != null) {
+ kekObjects = kekObjectDao.listByPurpose(purpose);
+ } else {
+ kekObjects = kekObjectDao.listAll();
+ }
- // Return keys from cache
- for (String label : kekCache.keySet()) {
- if (purpose == null || label.startsWith(purpose.getName())) {
- keks.add(label);
+ for (KMSDatabaseKekObjectVO kekObject : kekObjects) {
+ if (kekObject.getRemoved() == null) {
+ keks.add(kekObject.getLabel());
}
}
+ logger.debug("listKeks called for purpose: {}. Found {} KEKs.", purpose, keks.size());
return keks;
} catch (Exception e) {
throw KMSException.kekOperationFailed("Failed to list KEKs: " + e.getMessage(), e);
@@ -186,9 +205,8 @@ public class DatabaseKMSProvider implements KMSProvider {
@Override
public boolean isKekAvailable(String kekId) throws KMSException {
try {
- String configKey = buildConfigKey(kekId);
- ConfigurationVO config = configDao.findByName(configKey);
- return config != null && config.getValue() != null;
+ KMSDatabaseKekObjectVO kekObject = kekObjectDao.findByLabel(kekId);
+ return kekObject != null && kekObject.getRemoved() == null && kekObject.getKeyMaterial() != null;
} catch (Exception e) {
logger.warn("Error checking KEK availability: {}", e.getMessage());
return false;
@@ -211,19 +229,10 @@ public class DatabaseKMSProvider implements KMSProvider {
// 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
- );
+ WrappedKey wrapped = new WrappedKey(kekLabel, purpose, ALGORITHM, wrappedBlob, PROVIDER_NAME, new Date(), null);
logger.debug("Wrapped {} key with KEK {}", purpose, kekLabel);
return wrapped;
-
} catch (Exception e) {
throw KMSException.wrapUnwrapFailed("Failed to wrap key: " + e.getMessage(), e);
} finally {
@@ -302,36 +311,11 @@ public class DatabaseKMSProvider implements KMSProvider {
@Override
public boolean healthCheck() throws KMSException {
try {
- // Verify we can access configuration
- if (configDao == null) {
- logger.error("Configuration DAO is not initialized");
+ // Verify we can access KEK object DAO
+ if (kekObjectDao == null) {
+ logger.error("KMSDatabaseKekObjectDao 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) {
@@ -339,52 +323,65 @@ public class DatabaseKMSProvider implements KMSProvider {
}
}
- // ==================== Private Helper Methods ====================
-
private byte[] loadKek(String kekLabel) throws KMSException {
// Check cache first
if (CacheEnabled.value()) {
byte[] cached = kekCache.get(kekLabel);
if (cached != null) {
+ updateLastUsed(kekLabel);
return Arrays.copyOf(cached, cached.length); // Return copy
}
}
// Load from database
- String configKey = buildConfigKey(kekLabel);
- ConfigurationVO config = configDao.findByName(configKey);
+ KMSDatabaseKekObjectVO kekObject = kekObjectDao.findByLabel(kekLabel);
- if (config == null) {
+ if (kekObject == null || kekObject.getRemoved() != null) {
throw KMSException.kekNotFound("KEK with label " + kekLabel + " not found");
}
try {
- // getValue() automatically decrypts
- String kekBase64 = config.getValue();
- if (StringUtils.isEmpty(kekBase64)) {
+ // Decrypt the key material
+ byte[] encryptedKekBytes = kekObject.getKeyMaterial();
+ if (encryptedKekBytes == null || encryptedKekBytes.length == 0) {
throw KMSException.kekNotFound("KEK value is empty for label " + kekLabel);
}
- byte[] kekBytes = java.util.Base64.getDecoder().decode(kekBase64);
+ // Decrypt using DBEncryptionUtil
+ String encryptedKek = new String(encryptedKekBytes, StandardCharsets.UTF_8);
+ String kekBase64 = DBEncryptionUtil.decrypt(encryptedKek);
+ byte[] kekBytes = Base64.getDecoder().decode(kekBase64);
// Cache for future use
if (CacheEnabled.value()) {
kekCache.put(kekLabel, Arrays.copyOf(kekBytes, kekBytes.length));
}
+ // Update last used timestamp
+ updateLastUsed(kekLabel);
+
return kekBytes;
} catch (IllegalArgumentException e) {
throw KMSException.kekOperationFailed("Invalid KEK encoding for label " + kekLabel, e);
+ } catch (Exception e) {
+ throw KMSException.kekOperationFailed("Failed to decrypt KEK for label " + kekLabel + ": " + e.getMessage(), e);
}
}
- private String buildConfigKey(String label) {
- return KEK_CONFIG_PREFIX + label;
+ private void updateLastUsed(String kekLabel) {
+ try {
+ KMSDatabaseKekObjectVO kekObject = kekObjectDao.findByLabel(kekLabel);
+ if (kekObject != null && kekObject.getRemoved() == null) {
+ kekObject.setLastUsed(new Date());
+ kekObjectDao.update(kekObject.getId(), kekObject);
+ }
+ } catch (Exception e) {
+ logger.debug("Failed to update last used timestamp for KEK {}: {}", kekLabel, e.getMessage());
+ }
}
private String generateKekLabel(KeyPurpose purpose) {
return purpose.getName() + "-kek-" + UUID.randomUUID().toString().substring(0, 8);
}
}
-
diff --git a/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/database/KMSDatabaseKekObjectVO.java b/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/database/KMSDatabaseKekObjectVO.java
new file mode 100644
index 00000000000..e598a2b0914
--- /dev/null
+++ b/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/database/KMSDatabaseKekObjectVO.java
@@ -0,0 +1,357 @@
+// 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.database;
+
+import com.cloud.utils.db.GenericDao;
+import org.apache.cloudstack.framework.kms.KeyPurpose;
+import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+import java.util.Date;
+import java.util.UUID;
+
+/**
+ * Database entity for KEK objects stored by the database KMS provider.
+ * Models PKCS#11 object attributes for cryptographic key storage.
+ *
+ * This table stores KEKs (Key Encryption Keys) in a PKCS#11-compatible format,
+ * allowing the database provider to mock PKCS#11 interface behavior.
+ */
+@Entity
+@Table(name = "kms_database_kek_objects")
+public class KMSDatabaseKekObjectVO {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private Long id;
+
+ @Column(name = "uuid", nullable = false, unique = true)
+ private String uuid;
+
+ // PKCS#11 Object Class (CKA_CLASS)
+ @Column(name = "object_class", nullable = false, length = 32)
+ private String objectClass = "CKO_SECRET_KEY";
+
+ // PKCS#11 Label (CKA_LABEL) - human-readable identifier
+ @Column(name = "label", nullable = false, length = 255)
+ private String label;
+
+ // PKCS#11 ID (CKA_ID) - application-defined identifier
+ @Column(name = "object_id", length = 64)
+ private byte[] objectId;
+
+ // PKCS#11 Key Type (CKA_KEY_TYPE)
+ @Column(name = "key_type", nullable = false, length = 32)
+ private String keyType = "CKK_AES";
+
+ // PKCS#11 Key Value (CKA_VALUE) - encrypted KEK material
+ @Column(name = "key_material", nullable = false, length = 512)
+ private byte[] keyMaterial;
+
+ // PKCS#11 Boolean Attributes
+ @Column(name = "is_sensitive", nullable = false)
+ private Boolean isSensitive = true;
+
+ @Column(name = "is_extractable", nullable = false)
+ private Boolean isExtractable = false;
+
+ @Column(name = "is_token", nullable = false)
+ private Boolean isToken = true;
+
+ @Column(name = "is_private", nullable = false)
+ private Boolean isPrivate = true;
+
+ @Column(name = "is_modifiable", nullable = false)
+ private Boolean isModifiable = false;
+
+ @Column(name = "is_copyable", nullable = false)
+ private Boolean isCopyable = false;
+
+ @Column(name = "is_destroyable", nullable = false)
+ private Boolean isDestroyable = true;
+
+ @Column(name = "always_sensitive", nullable = false)
+ private Boolean alwaysSensitive = true;
+
+ @Column(name = "never_extractable", nullable = false)
+ private Boolean neverExtractable = true;
+
+ // Key Metadata
+ @Column(name = "purpose", nullable = false, length = 32)
+ @Enumerated(EnumType.STRING)
+ private KeyPurpose purpose;
+
+ @Column(name = "key_bits", nullable = false)
+ private Integer keyBits;
+
+ @Column(name = "algorithm", nullable = false, length = 64)
+ private String algorithm = "AES/GCM/NoPadding";
+
+ // PKCS#11 Validity Dates
+ @Column(name = "start_date")
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date startDate;
+
+ @Column(name = "end_date")
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date endDate;
+
+ // Lifecycle
+ @Column(name = GenericDao.CREATED_COLUMN, nullable = false)
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date created;
+
+ @Column(name = "last_used")
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date lastUsed;
+
+ @Column(name = GenericDao.REMOVED_COLUMN)
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date removed;
+
+ public KMSDatabaseKekObjectVO() {
+ this.uuid = UUID.randomUUID().toString();
+ this.created = new Date();
+ }
+
+ /**
+ * Constructor for creating a new KEK object
+ *
+ * @param label PKCS#11 label (CKA_LABEL)
+ * @param purpose key purpose
+ * @param keyBits key size in bits
+ * @param keyMaterial encrypted key material (CKA_VALUE)
+ */
+ public KMSDatabaseKekObjectVO(String label, KeyPurpose purpose, Integer keyBits, byte[] keyMaterial) {
+ this();
+ this.label = label;
+ this.purpose = purpose;
+ this.keyBits = keyBits;
+ this.keyMaterial = keyMaterial;
+ this.objectId = label.getBytes(); // Use label as object ID by default
+ this.startDate = new Date();
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ public void setUuid(String uuid) {
+ this.uuid = uuid;
+ }
+
+ public String getObjectClass() {
+ return objectClass;
+ }
+
+ public void setObjectClass(String objectClass) {
+ this.objectClass = objectClass;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public void setLabel(String label) {
+ this.label = label;
+ }
+
+ public byte[] getObjectId() {
+ return objectId;
+ }
+
+ public void setObjectId(byte[] objectId) {
+ this.objectId = objectId;
+ }
+
+ public String getKeyType() {
+ return keyType;
+ }
+
+ public void setKeyType(String keyType) {
+ this.keyType = keyType;
+ }
+
+ public byte[] getKeyMaterial() {
+ return keyMaterial;
+ }
+
+ public void setKeyMaterial(byte[] keyMaterial) {
+ this.keyMaterial = keyMaterial;
+ }
+
+ public Boolean getIsSensitive() {
+ return isSensitive;
+ }
+
+ public void setIsSensitive(Boolean isSensitive) {
+ this.isSensitive = isSensitive;
+ }
+
+ public Boolean getIsExtractable() {
+ return isExtractable;
+ }
+
+ public void setIsExtractable(Boolean isExtractable) {
+ this.isExtractable = isExtractable;
+ }
+
+ public Boolean getIsToken() {
+ return isToken;
+ }
+
+ public void setIsToken(Boolean isToken) {
+ this.isToken = isToken;
+ }
+
+ public Boolean getIsPrivate() {
+ return isPrivate;
+ }
+
+ public void setIsPrivate(Boolean isPrivate) {
+ this.isPrivate = isPrivate;
+ }
+
+ public Boolean getIsModifiable() {
+ return isModifiable;
+ }
+
+ public void setIsModifiable(Boolean isModifiable) {
+ this.isModifiable = isModifiable;
+ }
+
+ public Boolean getIsCopyable() {
+ return isCopyable;
+ }
+
+ public void setIsCopyable(Boolean isCopyable) {
+ this.isCopyable = isCopyable;
+ }
+
+ public Boolean getIsDestroyable() {
+ return isDestroyable;
+ }
+
+ public void setIsDestroyable(Boolean isDestroyable) {
+ this.isDestroyable = isDestroyable;
+ }
+
+ public Boolean getAlwaysSensitive() {
+ return alwaysSensitive;
+ }
+
+ public void setAlwaysSensitive(Boolean alwaysSensitive) {
+ this.alwaysSensitive = alwaysSensitive;
+ }
+
+ public Boolean getNeverExtractable() {
+ return neverExtractable;
+ }
+
+ public void setNeverExtractable(Boolean neverExtractable) {
+ this.neverExtractable = neverExtractable;
+ }
+
+ public KeyPurpose getPurpose() {
+ return purpose;
+ }
+
+ public void setPurpose(KeyPurpose purpose) {
+ this.purpose = purpose;
+ }
+
+ public Integer getKeyBits() {
+ return keyBits;
+ }
+
+ public void setKeyBits(Integer keyBits) {
+ this.keyBits = keyBits;
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ public void setAlgorithm(String algorithm) {
+ this.algorithm = algorithm;
+ }
+
+ public Date getStartDate() {
+ return startDate;
+ }
+
+ public void setStartDate(Date startDate) {
+ this.startDate = startDate;
+ }
+
+ public Date getEndDate() {
+ return endDate;
+ }
+
+ public void setEndDate(Date endDate) {
+ this.endDate = endDate;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public Date getLastUsed() {
+ return lastUsed;
+ }
+
+ public void setLastUsed(Date lastUsed) {
+ this.lastUsed = lastUsed;
+ }
+
+ public Date getRemoved() {
+ return removed;
+ }
+
+ public void setRemoved(Date removed) {
+ this.removed = removed;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("KMSDatabaseKekObject %s", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(
+ this, "id", "uuid", "label", "purpose", "keyBits", "objectClass", "keyType", "algorithm"));
+ }
+}
diff --git a/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/database/dao/KMSDatabaseKekObjectDao.java b/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/database/dao/KMSDatabaseKekObjectDao.java
new file mode 100644
index 00000000000..582c1179ec4
--- /dev/null
+++ b/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/database/dao/KMSDatabaseKekObjectDao.java
@@ -0,0 +1,61 @@
+// 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.database.dao;
+
+import com.cloud.utils.db.GenericDao;
+import org.apache.cloudstack.framework.kms.KeyPurpose;
+import org.apache.cloudstack.kms.provider.database.KMSDatabaseKekObjectVO;
+
+import java.util.List;
+
+/**
+ * DAO for KMSDatabaseKekObject entities
+ * Provides PKCS#11-like object storage operations for KEKs
+ */
+public interface KMSDatabaseKekObjectDao extends GenericDao {
+
+ /**
+ * Find a KEK object by label (PKCS#11 CKA_LABEL)
+ */
+ KMSDatabaseKekObjectVO findByLabel(String label);
+
+ /**
+ * Find a KEK object by object ID (PKCS#11 CKA_ID)
+ */
+ KMSDatabaseKekObjectVO findByObjectId(byte[] objectId);
+
+ /**
+ * List all KEK objects by purpose
+ */
+ List listByPurpose(KeyPurpose purpose);
+
+ /**
+ * List all KEK objects by key type (PKCS#11 CKA_KEY_TYPE)
+ */
+ List listByKeyType(String keyType);
+
+ /**
+ * List all KEK objects by object class (PKCS#11 CKA_CLASS)
+ */
+ List listByObjectClass(String objectClass);
+
+ /**
+ * Check if a KEK object exists with the given label
+ */
+ boolean existsByLabel(String label);
+}
diff --git a/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/database/dao/KMSDatabaseKekObjectDaoImpl.java b/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/database/dao/KMSDatabaseKekObjectDaoImpl.java
new file mode 100644
index 00000000000..ae65f3248b3
--- /dev/null
+++ b/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/database/dao/KMSDatabaseKekObjectDaoImpl.java
@@ -0,0 +1,84 @@
+// 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.database.dao;
+
+import com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+import org.apache.cloudstack.framework.kms.KeyPurpose;
+import org.apache.cloudstack.kms.provider.database.KMSDatabaseKekObjectVO;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class KMSDatabaseKekObjectDaoImpl extends GenericDaoBase implements KMSDatabaseKekObjectDao {
+
+ private final SearchBuilder allFieldSearch;
+
+ public KMSDatabaseKekObjectDaoImpl() {
+ allFieldSearch = createSearchBuilder();
+ allFieldSearch.and("uuid", allFieldSearch.entity().getUuid(), SearchCriteria.Op.EQ);
+ allFieldSearch.and("label", allFieldSearch.entity().getLabel(), SearchCriteria.Op.EQ);
+ allFieldSearch.and("objectId", allFieldSearch.entity().getObjectId(), SearchCriteria.Op.EQ);
+ allFieldSearch.and("purpose", allFieldSearch.entity().getPurpose(), SearchCriteria.Op.EQ);
+ allFieldSearch.and("keyType", allFieldSearch.entity().getKeyType(), SearchCriteria.Op.EQ);
+ allFieldSearch.and("objectClass", allFieldSearch.entity().getObjectClass(), SearchCriteria.Op.EQ);
+ allFieldSearch.done();
+ }
+
+ @Override
+ public KMSDatabaseKekObjectVO findByLabel(String label) {
+ SearchCriteria sc = allFieldSearch.create();
+ sc.setParameters("label", label);
+ return findOneBy(sc);
+ }
+
+ @Override
+ public KMSDatabaseKekObjectVO findByObjectId(byte[] objectId) {
+ SearchCriteria sc = allFieldSearch.create();
+ sc.setParameters("objectId", objectId);
+ return findOneBy(sc);
+ }
+
+ @Override
+ public List listByPurpose(KeyPurpose purpose) {
+ SearchCriteria sc = allFieldSearch.create();
+ sc.setParameters("purpose", purpose);
+ return listBy(sc);
+ }
+
+ @Override
+ public List listByKeyType(String keyType) {
+ SearchCriteria sc = allFieldSearch.create();
+ sc.setParameters("keyType", keyType);
+ return listBy(sc);
+ }
+
+ @Override
+ public List listByObjectClass(String objectClass) {
+ SearchCriteria sc = allFieldSearch.create();
+ sc.setParameters("objectClass", objectClass);
+ return listBy(sc);
+ }
+
+ @Override
+ public boolean existsByLabel(String label) {
+ return findByLabel(label) != null;
+ }
+}
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
index 57d436bcea5..ec7bbd38b04 100644
--- 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
@@ -16,5 +16,4 @@
# under the License.
name=database-kms
-parent=kmsProvidersRegistry
-
+parent=kms
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
index be2e666a74d..5ec8d157918 100644
--- 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
@@ -16,21 +16,18 @@
specific language governing permissions and limitations
under the License.
-->
-
-
-
+
-
+
-
diff --git a/plugins/kms/pom.xml b/plugins/kms/pom.xml
index afff4024e96..fee2c654565 100644
--- a/plugins/kms/pom.xml
+++ b/plugins/kms/pom.xml
@@ -17,8 +17,8 @@
specific language governing permissions and limitations
under the License.
-->
-
4.0.0
cloudstack-kms-plugins
diff --git a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java
index 5faa377ce3d..a5e87870eab 100644
--- a/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java
+++ b/plugins/storage/volume/default/src/main/java/org/apache/cloudstack/storage/datastore/driver/CloudStackPrimaryDataStoreDriverImpl.java
@@ -575,11 +575,25 @@ public class CloudStackPrimaryDataStoreDriverImpl implements PrimaryDataStoreDri
*/
private boolean anyVolumeRequiresEncryption(DataObject ... objects) {
for (DataObject o : objects) {
- // this fails code smell for returning true twice, but it is more readable than combining all tests into one statement
- if (o instanceof VolumeInfo && ((VolumeInfo) o).getPassphraseId() != null) {
- return true;
- } else if (o instanceof SnapshotInfo && ((SnapshotInfo) o).getBaseVolume().getPassphraseId() != null) {
- return true;
+ // Check for legacy passphrase-based encryption
+ if (o instanceof VolumeInfo) {
+ VolumeInfo vol = (VolumeInfo) o;
+ if (vol.getPassphraseId() != null) {
+ return true;
+ }
+ // Check for KMS-based encryption
+ if (vol.getKmsWrappedKeyId() != null || vol.getKmsKeyId() != null) {
+ return true;
+ }
+ } else if (o instanceof SnapshotInfo) {
+ VolumeInfo baseVol = ((SnapshotInfo) o).getBaseVolume();
+ if (baseVol.getPassphraseId() != null) {
+ return true;
+ }
+ // Check for KMS-based encryption
+ if (baseVol.getKmsWrappedKeyId() != null || baseVol.getKmsKeyId() != null) {
+ return true;
+ }
}
}
return false;
diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
index 17961dbd955..dcfacfd897d 100644
--- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
+++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java
@@ -963,7 +963,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
String userSpecifiedName = getVolumeNameFromCommand(cmd);
return commitVolume(cmd.getSnapshotId(), caller, owner, displayVolume, zoneId, diskOfferingId, provisioningType, size, minIops, maxIops, parentVolume, userSpecifiedName,
- _uuidMgr.generateUuid(Volume.class, cmd.getCustomId()), details);
+ _uuidMgr.generateUuid(Volume.class, cmd.getCustomId()), details, cmd.getKmsKeyId());
}
@Override
@@ -977,7 +977,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
}
private VolumeVO commitVolume(final Long snapshotId, final Account caller, final Account owner, final Boolean displayVolume, final Long zoneId, final Long diskOfferingId,
- final Storage.ProvisioningType provisioningType, final Long size, final Long minIops, final Long maxIops, final VolumeVO parentVolume, final String userSpecifiedName, final String uuid, final Map details) {
+ final Storage.ProvisioningType provisioningType, final Long size, final Long minIops, final Long maxIops, final VolumeVO parentVolume, final String userSpecifiedName, final String uuid, final Map details, final Long kmsKeyId) {
return Transaction.execute(new TransactionCallback() {
@Override
public VolumeVO doInTransaction(TransactionStatus status) {
@@ -1023,6 +1023,12 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
}
}
+ // Store KMS key ID if provided (for volume encryption)
+ if (volume != null && kmsKeyId != null) {
+ volume.setKmsKeyId(kmsKeyId);
+ _volsDao.update(volume.getId(), volume);
+ }
+
CallContext.current().setEventDetails("Volume ID: " + volume.getUuid());
CallContext.current().putContextParameter(Volume.class, volume.getId());
// Increment resource count during allocation; if actual creation fails,
@@ -2679,7 +2685,7 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
}
DiskOfferingVO diskOffering = _diskOfferingDao.findById(volumeToAttach.getDiskOfferingId());
- if (diskOffering.getEncrypt() && rootDiskHyperType != HypervisorType.KVM) {
+ if (diskOffering.getEncrypt() && !(rootDiskHyperType == HypervisorType.KVM)) {
throw new InvalidParameterValueException("Volume's disk offering has encryption enabled, but volume encryption is not supported for hypervisor type " + rootDiskHyperType);
}
diff --git a/server/src/main/java/org/apache/cloudstack/kms/KMSManagerImpl.java b/server/src/main/java/org/apache/cloudstack/kms/KMSManagerImpl.java
index 89291f48ad7..4727c34ce75 100644
--- a/server/src/main/java/org/apache/cloudstack/kms/KMSManagerImpl.java
+++ b/server/src/main/java/org/apache/cloudstack/kms/KMSManagerImpl.java
@@ -21,12 +21,17 @@ import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
+import com.cloud.utils.EnumUtils;
+import com.cloud.utils.Pair;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.component.PluggableService;
+import com.cloud.exception.PermissionDeniedException;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.admin.kms.MigrateVolumesToKMSCmd;
+import org.apache.cloudstack.api.command.admin.kms.RotateKMSKeyCmd;
import org.apache.cloudstack.api.command.user.kms.CreateKMSKeyCmd;
import org.apache.cloudstack.api.command.user.kms.DeleteKMSKeyCmd;
import org.apache.cloudstack.api.command.user.kms.ListKMSKeysCmd;
@@ -43,22 +48,24 @@ import org.apache.cloudstack.framework.kms.WrappedKey;
import org.apache.cloudstack.kms.dao.KMSKekVersionDao;
import org.apache.cloudstack.kms.dao.KMSKeyDao;
import org.apache.cloudstack.kms.dao.KMSWrappedKeyDao;
+import org.apache.cloudstack.secret.PassphraseVO;
+import org.apache.cloudstack.secret.dao.PassphraseDao;
+import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import com.cloud.storage.VolumeVO;
+import com.cloud.storage.dao.VolumeDao;
import javax.inject.Inject;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
-/**
- * Implementation of KMS Manager.
- * Provides high-level KMS operations with provider abstraction, zone-scoping,
- * retry logic, and audit logging.
- */
public class KMSManagerImpl extends ManagerBase implements KMSManager, PluggableService {
private static final Logger logger = LogManager.getLogger(KMSManagerImpl.class);
private static final Map kmsProviderMap = new HashMap<>();
@@ -73,6 +80,10 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
private AccountManager accountManager;
@Inject
private ResponseGenerator responseGenerator;
+ @Inject
+ private VolumeDao volumeDao;
+ @Inject
+ private PassphraseDao passphraseDao;
private List kmsProviders;
// ==================== Provider Management ====================
@@ -139,12 +150,18 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
public void deleteKek(Long zoneId, String kekId) throws KMSException {
validateKmsEnabled(zoneId);
- // TODO: Check if any wrapped keys use this KEK
- // This requires finding KMSKeyVO by kekLabel first, then checking wrapped keys
- // For now, allow deletion (will be fixed in Phase 5)
-
KMSProvider provider = getKMSProviderForZone(zoneId);
+ // Check if any wrapped keys use this KEK
+ KMSKeyVO key = kmsKeyDao.findByKekLabel(kekId, provider.getProviderName());
+ if (key != null) {
+ long wrappedKeyCount = kmsKeyDao.countWrappedKeysByKmsKey(key.getId());
+ if (wrappedKeyCount > 0) {
+ throw KMSException.invalidParameter("Cannot delete KEK: " + wrappedKeyCount +
+ " wrapped key(s) still reference the corresponding KMS key");
+ }
+ }
+
try {
logger.warn("Deleting KEK {} for zone {}", kekId, zoneId);
retryOperation(() -> {
@@ -187,7 +204,6 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
}
@Override
- @ActionEvent(eventType = EventTypes.EVENT_KMS_KEK_ROTATE, eventDescription = "rotating KEK", async = true)
public String rotateKek(Long zoneId, KeyPurpose purpose, String oldKekLabel,
String newKekLabel, int keyBits) throws KMSException {
validateKmsEnabled(zoneId);
@@ -220,12 +236,11 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
KMSKekVersionVO newVersion = createKekVersion(kmsKey.getId(), newKekId, keyBits);
logger.info("KEK rotation: KMS key {} now has {} versions (active: v{}, previous: v{})",
- kmsKey.getUuid(), newVersion.getVersionNumber(), newVersion.getVersionNumber(),
+ kmsKey, newVersion.getVersionNumber(), newVersion.getVersionNumber(),
newVersion.getVersionNumber() - 1);
- // TODO: Schedule background job to rewrap all DEKs (Phase 5)
+ // Schedule background job to rewrap all DEKs
// This will gradually rewrap wrapped keys to use the new KEK version
-
return newKekId;
} catch (Exception e) {
@@ -241,10 +256,6 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
public byte[] unwrapVolumeKey(WrappedKey wrappedKey, Long zoneId) throws KMSException {
validateKmsEnabled(zoneId);
- return unwrapDek(wrappedKey);
- }
-
- private byte[] unwrapDek(WrappedKey wrappedKey) throws KMSException {
// Determine provider from wrapped key
String providerName = wrappedKey.getProviderName();
KMSProvider provider = getKMSProvider(providerName);
@@ -306,66 +317,64 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
KMSKekVersionVO.Status.Active);
initialVersion = kmsKekVersionDao.persist(initialVersion);
- logger.info("Created KMS key '{}' (UUID: {}) with initial KEK version {} for account {} in zone {}",
- name, kmsKey.getUuid(), initialVersion.getVersionNumber(), accountId, zoneId);
+ logger.info("Created KMS key ({}) with initial KEK version {} for account {} in zone {}",
+ kmsKey, initialVersion.getVersionNumber(), accountId, zoneId);
return kmsKey;
}
@Override
public List extends KMSKey> listUserKMSKeys(Long accountId, Long domainId, Long zoneId,
KeyPurpose purpose, KMSKey.State state) {
- // List keys accessible to the account (owned by account or in domain)
return kmsKeyDao.listAccessibleKeys(accountId, domainId, zoneId, purpose, state);
}
- // ==================== Health Check ====================
-
@Override
public KMSKey getUserKMSKey(String uuid, Long callerAccountId) {
KMSKeyVO key = kmsKeyDao.findByUuid(uuid);
if (key == null || key.getState() == KMSKey.State.Deleted) {
return null;
}
- // Check permission
- if (!hasPermission(callerAccountId, uuid)) {
+
+ if (!hasPermission(callerAccountId, key)) {
return null;
}
return key;
}
- // ==================== Helper Methods ====================
-
@Override
- public boolean hasPermission(Long callerAccountId, String keyUuid) {
- KMSKeyVO key = kmsKeyDao.findByUuid(keyUuid);
+ public boolean hasPermission(Long callerAccountId, KMSKey key) {
if (key == null || key.getState() == KMSKey.State.Deleted) {
return false;
}
- // Owner always has permission
if (key.getAccountId() == callerAccountId) {
return true;
}
- // TODO: Domain admin can access keys in their domain/subdomains
- // For now, only owner has permission
- return false;
- }
+ Account caller = accountManager.getAccount(callerAccountId);
+ Account owner = accountManager.getAccount(key.getAccountId());
- @Override
- @ActionEvent(eventType = EventTypes.EVENT_KMS_KEK_DELETE, eventDescription = "deleting user KMS key", async = false)
- public void deleteUserKMSKey(String uuid, Long callerAccountId) throws KMSException {
- KMSKeyVO key = kmsKeyDao.findByUuid(uuid);
- if (key == null) {
- throw KMSException.kekNotFound("KMS key not found: " + uuid);
+ if (caller == null || owner == null) {
+ return false;
}
- // Check permission
- if (!hasPermission(callerAccountId, uuid)) {
- throw KMSException.invalidParameter("No permission to delete KMS key: " + uuid);
+ try {
+ accountManager.checkAccess(caller, null, true, owner);
+ return true;
+ } catch (PermissionDeniedException e) {
+ return false;
+ }
+ }
+
+ private void deleteUserKMSKey(KMSKeyVO key, Long callerAccountId) throws KMSException {
+ if (!hasPermission(callerAccountId, key)) {
+ throw KMSException.invalidParameter("No permission to delete KMS key: " + key.getUuid());
}
// Check if key is in use
+
+ // TODO: Check if there are any volumes linked with the kms key and delete accordingly.
+ // The below check seems incorrect here.
long wrappedKeyCount = kmsKeyDao.countWrappedKeysByKmsKey(key.getId());
if (wrappedKeyCount > 0) {
throw KMSException.invalidParameter("Cannot delete KMS key: " + wrappedKeyCount +
@@ -374,27 +383,16 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
// Soft delete
key.setState(KMSKey.State.Deleted);
- key.setRemoved(new java.util.Date());
+ key.setRemoved(new Date());
kmsKeyDao.update(key.getId(), key);
- // Optionally delete KEK from provider (but keep metadata for audit)
- // provider.deleteKek(key.getKekLabel());
-
- logger.info("Deleted KMS key '{}' (UUID: {})", key.getName(), uuid);
+ logger.info("Deleted KMS key {}", key);
}
- @Override
- @ActionEvent(eventType = EventTypes.EVENT_KMS_KEK_CREATE, eventDescription = "updating user KMS key", async = false)
- public KMSKey updateUserKMSKey(String uuid, Long callerAccountId,
+ private KMSKey updateUserKMSKey(KMSKeyVO key, Long callerAccountId,
String name, String description, KMSKey.State state) throws KMSException {
- KMSKeyVO key = kmsKeyDao.findByUuid(uuid);
- if (key == null) {
- throw KMSException.kekNotFound("KMS key not found: " + uuid);
- }
-
- // Check permission
- if (!hasPermission(callerAccountId, uuid)) {
- throw KMSException.invalidParameter("No permission to update KMS key: " + uuid);
+ if (!hasPermission(callerAccountId, key)) {
+ throw KMSException.invalidParameter("No permission to update KMS key: " + key.getUuid());
}
boolean updated = false;
@@ -416,7 +414,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
if (updated) {
kmsKeyDao.update(key.getId(), key);
- logger.info("Updated KMS key '{}' (UUID: {})", key.getName(), uuid);
+ logger.info("Updated KMS key {}", key);
}
return key;
@@ -458,7 +456,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
}
// Fallback: try all available versions for decryption
- List versions = getKekVersionsForDecryption(kmsKey.getId());
+ List versions = kmsKekVersionDao.getVersionsForDecryption(kmsKey.getId());
for (KMSKekVersionVO version : versions) {
try {
WrappedKey wrapped = new WrappedKey(version.getKekLabel(), kmsKey.getPurpose(),
@@ -478,32 +476,23 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
// ==================== Lifecycle Methods ====================
- /**
- * Get all KEK versions that can be used for decryption (Active and Previous)
- */
- private List getKekVersionsForDecryption(Long kmsKeyId) {
- return kmsKekVersionDao.getVersionsForDecryption(kmsKeyId);
- }
-
@Override
@ActionEvent(eventType = EventTypes.EVENT_KMS_KEY_WRAP,
eventDescription = "generating volume key with specified KEK", async = false)
- public WrappedKey generateVolumeKeyWithKek(String kekUuid, Long callerAccountId) throws KMSException {
+ public WrappedKey generateVolumeKeyWithKek(KMSKey kmsKey, Long callerAccountId) throws KMSException {
// Get and validate KMS key
- KMSKey kmsKey = getUserKMSKey(kekUuid, callerAccountId);
if (kmsKey == null) {
- throw KMSException.kekNotFound("KMS key not found or no permission: " + kekUuid);
+ throw KMSException.kekNotFound("KMS key not found");
}
if (kmsKey.getState() != KMSKey.State.Enabled) {
- throw KMSException.invalidParameter("KMS key is not enabled: " + kekUuid);
+ throw KMSException.invalidParameter("KMS key is not enabled: " + kmsKey);
}
if (kmsKey.getPurpose() != KeyPurpose.VOLUME_ENCRYPTION) {
- throw KMSException.invalidParameter("KMS key purpose is not VOLUME_ENCRYPTION: " + kekUuid);
+ throw KMSException.invalidParameter("KMS key purpose is not VOLUME_ENCRYPTION: " + kmsKey);
}
- // Get provider
KMSProvider provider = getKMSProviderForZone(kmsKey.getZoneId());
// Get active KEK version
@@ -522,7 +511,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
// Return WrappedKey with database UUID so it can be looked up later
// Note: Volume creation code should look up by UUID and set volume.kmsWrappedKeyId
- WrappedKey persistedWrappedKey = new WrappedKey(
+ wrappedKey = new WrappedKey(
wrappedKeyVO.getUuid(),
wrappedKey.getKekId(),
wrappedKey.getPurpose(),
@@ -532,13 +521,12 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
wrappedKey.getCreated(),
wrappedKey.getZoneId()
);
- wrappedKey = persistedWrappedKey;
} catch (Exception e) {
throw handleKmsException(e);
}
- logger.debug("Generated volume key using KMS key '{}' (UUID: {}) with KEK version {}, wrapped key UUID: {}",
- kmsKey.getName(), kekUuid, activeVersion.getVersionNumber(), wrappedKey.getId());
+ logger.debug("Generated volume key using KMS key {} with KEK version {}, wrapped key UUID: {}",
+ kmsKey, activeVersion.getVersionNumber(), wrappedKey.getUuid());
return wrappedKey;
}
@@ -560,7 +548,6 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
Account caller = CallContext.current().getCallingAccount();
Account targetAccount = caller;
- // If account/domain specified, validate permissions and resolve account
if (cmd.getAccountName() != null || cmd.getDomainId() != null) {
// Only admins and domain admins can create keys for other accounts
if (!accountManager.isAdmin(caller.getId()) &&
@@ -575,7 +562,6 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
throw KMSException.invalidParameter(
"Unable to find account " + cmd.getAccountName() + " in domain " + cmd.getDomainId());
}
- // Check access
accountManager.checkAccess(caller, null, true, targetAccount);
} else {
throw KMSException.invalidParameter("Both accountName and domainId must be specified together");
@@ -588,7 +574,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
keyPurpose = KeyPurpose.fromString(cmd.getPurpose());
} catch (IllegalArgumentException e) {
throw KMSException.invalidParameter("Invalid purpose: " + cmd.getPurpose() +
- ". Valid values: VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET");
+ ". Valid values: volume, tls");
}
// Validate key bits
@@ -617,52 +603,42 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
public ListResponse listKMSKeys(ListKMSKeysCmd cmd) {
Account caller = CallContext.current().getCallingAccount();
if (caller == null) {
- ListResponse response = new ListResponse<>();
- response.setResponses(new java.util.ArrayList<>(), 0);
- return response;
+ return createEmptyListResponse();
}
- // Parse purpose if provided
KeyPurpose keyPurpose = null;
if (cmd.getPurpose() != null) {
try {
keyPurpose = KeyPurpose.fromString(cmd.getPurpose());
} catch (IllegalArgumentException e) {
- // Invalid purpose - will be ignored
+ throw KMSException.invalidParameter("Invalid purpose: " + cmd.getPurpose() + ". Valid values: volume, tls");
}
}
- // Parse state if provided
KMSKey.State keyState = null;
if (cmd.getState() != null) {
- try {
- keyState = KMSKey.State.valueOf(cmd.getState());
- } catch (IllegalArgumentException e) {
- // Invalid state - will be ignored
+ keyState = EnumUtils.getEnumIgnoreCase(KMSKey.State.class, cmd.getState());
+ if (keyState == null) {
+ throw KMSException.invalidParameter("Invalid state: " + cmd.getState() + ". Valid values: Enabled, Disabled");
}
}
- // If specific ID requested
if (cmd.getId() != null) {
- // Look up key by ID to get UUID
KMSKeyVO key = kmsKeyDao.findById(cmd.getId());
- if (key == null) {
- // Key not found - return empty list
+ if (key == null || key.getState() == KMSKey.State.Deleted) {
+ return createEmptyListResponse();
+ }
+
+ if (hasPermission(caller.getId(), key)) {
+ List responses = new ArrayList<>();
+ responses.add(responseGenerator.createKMSKeyResponse(key));
ListResponse listResponse = new ListResponse<>();
- listResponse.setResponses(new java.util.ArrayList<>(), 0);
+ listResponse.setResponses(responses, responses.size());
return listResponse;
}
- KMSKey kmsKey = getUserKMSKey(key.getUuid(), caller.getId());
- List responses = new java.util.ArrayList<>();
- if (kmsKey != null && hasPermission(caller.getId(), kmsKey.getUuid())) {
- responses.add(responseGenerator.createKMSKeyResponse(kmsKey));
- }
- ListResponse listResponse = new ListResponse<>();
- listResponse.setResponses(responses, responses.size());
- return listResponse;
+ return createEmptyListResponse();
}
- // List accessible keys
List extends KMSKey> keys = listUserKMSKeys(
caller.getId(),
caller.getDomainId(),
@@ -671,7 +647,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
keyState
);
- List responses = new java.util.ArrayList<>();
+ List responses = new ArrayList<>();
for (KMSKey key : keys) {
responses.add(responseGenerator.createKMSKeyResponse(key));
}
@@ -685,27 +661,23 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
public KMSKeyResponse updateKMSKey(UpdateKMSKeyCmd cmd) throws KMSException {
Long callerAccountId = CallContext.current().getCallingAccount().getId();
- // Parse state if provided
KMSKey.State keyState = null;
if (cmd.getState() != null) {
- try {
- keyState = KMSKey.State.valueOf(cmd.getState());
- if (keyState == KMSKey.State.Deleted) {
- throw KMSException.invalidParameter("Cannot set state to Deleted. Use deleteKMSKey instead.");
- }
- } catch (IllegalArgumentException e) {
- throw KMSException.invalidParameter(
- "Invalid state: " + cmd.getState() + ". Valid values: Enabled, Disabled");
+ keyState = EnumUtils.getEnumIgnoreCase(KMSKey.State.class, cmd.getState());
+ if (keyState == KMSKey.State.Deleted) {
+ throw KMSException.invalidParameter("Cannot set state to Deleted. Use deleteKMSKey instead.");
+ }
+ if (keyState == null) {
+ throw KMSException.invalidParameter("Invalid state: " + cmd.getState() + ". Valid values: Enabled, Disabled");
}
}
- // Look up key by ID to get UUID
KMSKeyVO key = kmsKeyDao.findById(cmd.getId());
if (key == null) {
throw KMSException.kekNotFound("KMS key not found: " + cmd.getId());
}
- KMSKey updatedKey = updateUserKMSKey(key.getUuid(), callerAccountId,
+ KMSKey updatedKey = updateUserKMSKey(key, callerAccountId,
cmd.getName(), cmd.getDescription(), keyState);
return responseGenerator.createKMSKeyResponse(updatedKey);
}
@@ -714,15 +686,13 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
public SuccessResponse deleteKMSKey(DeleteKMSKeyCmd cmd) throws KMSException {
Long callerAccountId = CallContext.current().getCallingAccount().getId();
- // Look up key by ID to get UUID
KMSKeyVO key = kmsKeyDao.findById(cmd.getId());
if (key == null) {
throw KMSException.kekNotFound("KMS key not found: " + cmd.getId());
}
- deleteUserKMSKey(key.getUuid(), callerAccountId);
- SuccessResponse response = new SuccessResponse();
- return response;
+ deleteUserKMSKey(key, callerAccountId);
+ return new SuccessResponse();
}
// ==================== User KEK Management ====================
@@ -754,6 +724,272 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
return newVersion;
}
+ // ==================== Admin Operations ====================
+
+ @Override
+ @ActionEvent(eventType = EventTypes.EVENT_KMS_KEK_ROTATE, eventDescription = "rotating KMS key", async = true)
+ public String rotateKMSKey(RotateKMSKeyCmd cmd) throws KMSException {
+ Integer keyBits = cmd.getKeyBits();
+
+ KMSKeyVO kmsKey = kmsKeyDao.findById(cmd.getId());
+ if (kmsKey == null) {
+ throw KMSException.kekNotFound("KMS key not found: " + cmd.getId());
+ }
+
+ if (kmsKey.getState() != KMSKey.State.Enabled) {
+ throw KMSException.invalidParameter("KMS key is not enabled: " + kmsKey);
+ }
+
+ // Get current active version to determine key bits if not provided
+ int newKeyBits = keyBits != null ? keyBits : kmsKey.getKeyBits();
+ KMSKekVersionVO currentActive = getActiveKekVersion(kmsKey.getId());
+
+ rotateKek(
+ kmsKey.getZoneId(),
+ kmsKey.getPurpose(),
+ currentActive.getKekLabel(),
+ null, // auto-generate new label
+ newKeyBits
+ );
+
+ KMSKekVersionVO newVersion = getActiveKekVersion(kmsKey.getId());
+
+ logger.info("KMS key rotation completed: {} -> new KEK version {} (UUID: {})",
+ kmsKey, newVersion.getVersionNumber(), newVersion.getUuid());
+
+ // Perform rewrapping of existing wrapped keys
+ // This runs within the async job context
+ rewrapWrappedKeysForKMSKey(kmsKey.getId(), newVersion.getId(), 50);
+
+ return newVersion.getUuid();
+ }
+
+ @Override
+ public int rewrapWrappedKeysForKMSKey(Long kmsKeyId, Long newKekVersionId, int batchSize) throws KMSException {
+ if (kmsKeyId == null || newKekVersionId == null) {
+ throw KMSException.invalidParameter("kmsKeyId and newKekVersionId must be specified");
+ }
+
+ if (batchSize <= 0) {
+ batchSize = 50; // Default batch size
+ }
+
+ // Get KMS key and new version
+ KMSKeyVO kmsKey = kmsKeyDao.findById(kmsKeyId);
+ if (kmsKey == null) {
+ throw KMSException.kekNotFound("KMS key not found: " + kmsKeyId);
+ }
+
+ KMSKekVersionVO newVersion = kmsKekVersionDao.findById(newKekVersionId);
+ if (newVersion == null || !newVersion.getKmsKeyId().equals(kmsKeyId)) {
+ throw KMSException.kekNotFound("KEK version not found or doesn't belong to KMS key: " + newKekVersionId);
+ }
+
+ KMSProvider provider = getKMSProviderForZone(kmsKey.getZoneId());
+
+ // Get all wrapped keys that need rewrap
+ List wrappedKeys = kmsWrappedKeyDao.listWrappedKeysForRewrap(kmsKeyId, newKekVersionId);
+ int totalKeys = wrappedKeys.size();
+ int successCount = 0;
+ int failureCount = 0;
+
+ logger.info("Starting rewrap operation for {} wrapped keys (KMS key: {}, new version: {})",
+ totalKeys, kmsKey, newKekVersionId);
+
+ for (int i = 0; i < wrappedKeys.size(); i += batchSize) {
+ int endIndex = Math.min(i + batchSize, wrappedKeys.size());
+ List batch = wrappedKeys.subList(i, endIndex);
+
+ for (KMSWrappedKeyVO wrappedKeyVO : batch) {
+ byte[] dek = null;
+ try {
+ // Unwrap with old version
+ dek = unwrapKey(wrappedKeyVO.getId());
+
+ // Wrap the existing DEK with new active version
+ WrappedKey newWrapped = provider.wrapKey(
+ dek,
+ kmsKey.getPurpose(),
+ newVersion.getKekLabel()
+ );
+
+ wrappedKeyVO.setKekVersionId(newKekVersionId);
+ wrappedKeyVO.setWrappedBlob(newWrapped.getWrappedKeyMaterial());
+ kmsWrappedKeyDao.update(wrappedKeyVO.getId(), wrappedKeyVO);
+
+ successCount++;
+ logger.debug("Rewrapped key {} (batch {}/{})", wrappedKeyVO.getId(),
+ (i / batchSize) + 1, (totalKeys + batchSize - 1) / batchSize);
+ } catch (Exception e) {
+ failureCount++;
+ logger.warn("Failed to rewrap key {}: {}", wrappedKeyVO.getId(), e.getMessage());
+ } finally {
+ // Zeroize DEK
+ if (dek != null) {
+ Arrays.fill(dek, (byte) 0);
+ }
+ }
+ }
+
+ logger.info("Processed batch {}/{}: {} success, {} failures",
+ (i / batchSize) + 1, (totalKeys + batchSize - 1) / batchSize, successCount, failureCount);
+ }
+
+ // Archive old versions if no wrapped keys reference them
+ List oldVersions = kmsKekVersionDao.getVersionsForDecryption(kmsKeyId);
+ for (KMSKekVersionVO oldVersion : oldVersions) {
+ if (oldVersion.getStatus() == KMSKekVersionVO.Status.Previous) {
+ List keysUsingVersion = kmsWrappedKeyDao.listByKekVersionId(oldVersion.getId());
+ if (keysUsingVersion.isEmpty()) {
+ oldVersion.setStatus(KMSKekVersionVO.Status.Archived);
+ kmsKekVersionDao.update(oldVersion.getId(), oldVersion);
+ logger.info("Archived KEK version {} (no wrapped keys using it)", oldVersion.getVersionNumber());
+ }
+ }
+ }
+
+ logger.info("Rewrap operation completed: {} success, {} failures out of {} total",
+ successCount, failureCount, totalKeys);
+
+ return successCount;
+ }
+
+ @Override
+ @ActionEvent(eventType = EventTypes.EVENT_VOLUME_MIGRATE_TO_KMS, eventDescription = "migrating volumes to KMS", async = true)
+ public int migrateVolumesToKMS(MigrateVolumesToKMSCmd cmd) throws KMSException {
+ Long zoneId = cmd.getZoneId();
+ String accountName = cmd.getAccountName();
+ Long domainId = cmd.getDomainId();
+
+ if (zoneId == null) {
+ throw KMSException.invalidParameter("zoneId must be specified");
+ }
+
+ validateKmsEnabled(zoneId);
+
+ Long accountId = null;
+ if (accountName != null) {
+ accountId = accountManager.finalyzeAccountId(accountName, domainId, null, true);
+ }
+
+ int pageSize = 100; // Process 100 volumes per page to avoid OutOfMemoryError
+
+ // Get provider
+ KMSProvider provider = getKMSProviderForZone(zoneId);
+
+ int successCount = 0;
+ int failureCount = 0;
+ logger.info("Starting migration of volumes to KMS (zone: {}, account: {}, domain: {})",
+ zoneId, accountId, domainId);
+
+ Pair, Integer> volumeListPair = volumeDao.listVolumesForKMSMigration(zoneId, accountId, domainId, pageSize);
+ List volumes = volumeListPair.first();
+ int totalCount = volumeListPair.second();
+
+ while (true) {
+
+ if (CollectionUtils.isEmpty(volumes) || totalCount == 0) {
+ break;
+ }
+
+ for (VolumeVO volume : volumes) {
+ try {
+ // Load passphrase
+ PassphraseVO passphrase = passphraseDao.findById(volume.getPassphraseId());
+ if (passphrase == null) {
+ logger.warn("Passphrase not found for volume {}: {}", volume.getId(), volume.getPassphraseId());
+ failureCount++;
+ continue;
+ }
+
+ // Get passphrase bytes
+ // Note: PassphraseVO.getPassphrase() returns Base64-encoded bytes
+ // This is consistent with how hypervisors (KVM/QEMU) expect the key format
+ // The KMS will store the same format, maintaining compatibility
+ byte[] passphraseBytes = passphrase.getPassphrase();
+
+ // Get or create KMS key for account
+ KMSKeyVO kmsKey = null;
+ List extends KMSKey> accountKeys = listUserKMSKeys(
+ volume.getAccountId(),
+ volume.getDomainId(),
+ zoneId,
+ KeyPurpose.VOLUME_ENCRYPTION,
+ KMSKey.State.Enabled
+ );
+
+ if (!accountKeys.isEmpty()) {
+ kmsKey = (KMSKeyVO) accountKeys.get(0); // Use first available key
+ } else {
+ // Create new KMS key for account
+ String keyName = "Volume-Encryption-Key-" + volume.getAccountId();
+ kmsKey = (KMSKeyVO) createUserKMSKey(
+ volume.getAccountId(),
+ volume.getDomainId(),
+ zoneId,
+ keyName,
+ "Auto-created for volume migration",
+ KeyPurpose.VOLUME_ENCRYPTION,
+ 256 // Default to 256 bits
+ );
+ logger.info("Created KMS key {} for account {} during migration", kmsKey, volume.getAccountId());
+ }
+
+ // Get active KEK version
+ KMSKekVersionVO activeVersion = getActiveKekVersion(kmsKey.getId());
+
+ // Wrap existing passphrase bytes as DEK (don't generate new DEK)
+ WrappedKey wrappedKey = provider.wrapKey(
+ passphraseBytes,
+ KeyPurpose.VOLUME_ENCRYPTION,
+ activeVersion.getKekLabel()
+ );
+
+ // Store wrapped key
+ KMSWrappedKeyVO wrappedKeyVO = new KMSWrappedKeyVO(
+ kmsKey.getId(),
+ activeVersion.getId(),
+ zoneId,
+ wrappedKey.getWrappedKeyMaterial()
+ );
+ wrappedKeyVO = kmsWrappedKeyDao.persist(wrappedKeyVO);
+
+ // Update volume
+ volume.setKmsWrappedKeyId(wrappedKeyVO.getId());
+ volume.setKmsKeyId(kmsKey.getId());
+ volume.setPassphraseId(null); // Clear passphrase reference
+ volumeDao.update(volume.getId(), volume);
+
+ // Zeroize passphrase bytes
+ if (passphraseBytes != null) {
+ Arrays.fill(passphraseBytes, (byte) 0);
+ }
+
+ successCount++;
+ logger.debug("Migrated volume's encryption {} to KMS (batch {})", volume, kmsKey);
+ } catch (Exception e) {
+ failureCount++;
+ logger.warn("Failed to migrate volume {}: {}", volume.getId(), e.getMessage());
+ // Continue with next volume
+ }
+ }
+
+ logger.debug("Processed {} volumes. success: {}, failure: {}", volumes.size(),
+ successCount, failureCount);
+ volumeListPair = volumeDao.listVolumesForKMSMigration(zoneId, accountId, domainId, pageSize);
+ volumes = volumeListPair.first();
+ if (totalCount == volumeListPair.second()) {
+ logger.debug("{} volumes pending for migration because passphrase was not found or migration failed", totalCount);
+ break;
+ }
+ totalCount = volumeListPair.second();
+ }
+ logger.info("Migration operation completed: {} total volumes processed, {} success, {} failures",
+ successCount + failureCount, successCount, failureCount);
+
+ return successCount;
+ }
+
private void validateKmsEnabled(Long zoneId) throws KMSException {
if (zoneId == null) {
throw KMSException.invalidParameter("Zone ID cannot be null");
@@ -787,7 +1023,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
attempt + 1, maxRetries + 1, e.getMessage());
try {
- Thread.sleep((long) retryDelay * (attempt + 1)); // Exponential backoff
+ Thread.sleep((long) retryDelay * (attempt + 1));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new CloudRuntimeException("Interrupted during retry", ie);
@@ -818,8 +1054,9 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
}
String providerName = KMSProviderPlugin.value();
- if (kmsProviderMap.containsKey(providerName) && kmsProviderMap.get(providerName) != null) {
- configuredKmsProvider = kmsProviderMap.get(providerName);
+ String providerKey = providerName != null ? providerName.toLowerCase() : null;
+ if (providerKey != null && kmsProviderMap.containsKey(providerKey) && kmsProviderMap.get(providerKey) != null) {
+ configuredKmsProvider = kmsProviderMap.get(providerKey);
return configuredKmsProvider;
}
@@ -833,9 +1070,22 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
// ==================== API Response Methods ====================
+ /**
+ * Helper method to create an empty list response
+ */
+ private ListResponse createEmptyListResponse() {
+ ListResponse response = new ListResponse<>();
+ response.setResponses(new ArrayList<>(), 0);
+ return response;
+ }
+
private void initializeKmsProviderMap() {
- if (kmsProviderMap != null && kmsProviderMap.size() != kmsProviders.size()) {
- for (KMSProvider provider : kmsProviders) {
+ if (kmsProviders == null) {
+ return;
+ }
+ kmsProviderMap.clear();
+ for (KMSProvider provider : kmsProviders) {
+ if (provider != null) {
kmsProviderMap.put(provider.getProviderName().toLowerCase(), provider);
logger.info("Registered KMS provider: {}", provider.getProviderName());
}
@@ -848,8 +1098,9 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
initializeKmsProviderMap();
String configuredProviderName = KMSProviderPlugin.value();
- if (kmsProviderMap.containsKey(configuredProviderName)) {
- configuredKmsProvider = kmsProviderMap.get(configuredProviderName);
+ String providerKey = configuredProviderName != null ? configuredProviderName.toLowerCase() : null;
+ if (providerKey != null && kmsProviderMap.containsKey(providerKey)) {
+ configuredKmsProvider = kmsProviderMap.get(providerKey);
logger.info("Configured KMS provider: {}", configuredKmsProvider.getProviderName());
}
@@ -898,6 +1149,8 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
cmdList.add(CreateKMSKeyCmd.class);
cmdList.add(UpdateKMSKeyCmd.class);
cmdList.add(DeleteKMSKeyCmd.class);
+ cmdList.add(RotateKMSKeyCmd.class);
+ cmdList.add(MigrateVolumesToKMSCmd.class);
return cmdList;
}
@@ -907,4 +1160,3 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
T execute() throws Exception;
}
}
-