integrate volume encryption with kms

This commit is contained in:
vishesh92 2025-12-23 11:39:41 +05:30
parent e995b46b20
commit e3a63ec932
No known key found for this signature in database
GPG Key ID: 4E395186CBFA790B
46 changed files with 1669 additions and 792 deletions

View File

@ -278,6 +278,7 @@ public class EventTypes {
public static final String EVENT_KMS_KEK_ROTATE = "KMS.KEK.ROTATE";
public static final String EVENT_KMS_KEK_DELETE = "KMS.KEK.DELETE";
public static final String EVENT_KMS_HEALTH_CHECK = "KMS.HEALTH.CHECK";
public static final String EVENT_VOLUME_MIGRATE_TO_KMS = "VOLUME.MIGRATE.TO.KMS";
// Account events
public static final String EVENT_ACCOUNT_ENABLE = "ACCOUNT.ENABLE";

View File

@ -275,6 +275,14 @@ public interface Volume extends ControlledEntity, Identity, InternalIdentity, Ba
void setPassphraseId(Long id);
Long getKmsKeyId();
void setKmsKeyId(Long id);
Long getKmsWrappedKeyId();
void setKmsWrappedKeyId(Long id);
String getEncryptFormat();
void setEncryptFormat(String encryptFormat);

View File

@ -0,0 +1,130 @@
// 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.api.command.admin.kms;
import com.cloud.user.Account;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseAsyncCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.AsyncJobResponse;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.kms.KMSManager;
import javax.inject.Inject;
@APICommand(name = "migrateVolumesToKMS",
description = "Migrates passphrase-based volumes to KMS (admin only)",
responseObject = AsyncJobResponse.class,
since = "4.23.0",
authorized = {RoleType.Admin},
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
public class MigrateVolumesToKMSCmd extends BaseAsyncCmd {
private static final String s_name = "migratevolumestokmsresponse";
@Inject
private KMSManager kmsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ZONE_ID,
required = true,
type = CommandType.UUID,
entityType = ZoneResponse.class,
description = "Zone ID")
private Long zoneId;
@Parameter(name = ApiConstants.ACCOUNT,
type = CommandType.STRING,
description = "Migrate volumes for specific account")
private String accountName;
@Parameter(name = ApiConstants.DOMAIN_ID,
type = CommandType.UUID,
entityType = DomainResponse.class,
description = "Domain ID")
private Long domainId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getZoneId() {
return zoneId;
}
public String getAccountName() {
return accountName;
}
public Long getDomainId() {
return domainId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
try {
kmsManager.migrateVolumesToKMS(this);
} catch (KMSException e) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR,
"Failed to migrate volumes to KMS: " + e.getMessage());
}
}
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public String getEventType() {
return com.cloud.event.EventTypes.EVENT_VOLUME_MIGRATE_TO_KMS;
}
@Override
public String getEventDescription() {
return "Migrating volumes to KMS for zone: " + _uuidMgr.getUuid(ZoneResponse.class, zoneId);
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.Zone;
}
@Override
public Long getApiResourceId() {
return zoneId;
}
}

View File

@ -0,0 +1,119 @@
// 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.api.command.admin.kms;
import com.cloud.user.Account;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseAsyncCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.AsyncJobResponse;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.kms.KMSManager;
import javax.inject.Inject;
@APICommand(name = "rotateKMSKey",
description = "Rotates KEK by creating new version and scheduling gradual re-encryption (admin only)",
responseObject = AsyncJobResponse.class,
since = "4.23.0",
authorized = {RoleType.Admin},
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
public class RotateKMSKeyCmd extends BaseAsyncCmd {
private static final String s_name = "rotatekmskeyresponse";
@Inject
private KMSManager kmsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID,
required = true,
type = CommandType.UUID,
entityType = KMSKeyResponse.class,
description = "KMS Key UUID to rotate")
private Long id;
@Parameter(name = ApiConstants.KEY_BITS,
type = CommandType.INTEGER,
description = "Key size for new KEK (default: same as current)")
private Integer keyBits;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
public Integer getKeyBits() {
return keyBits;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
try {
kmsManager.rotateKMSKey(this);
} catch (KMSException e) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR,
"Failed to rotate KMS key: " + e.getMessage());
}
}
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public String getEventType() {
return com.cloud.event.EventTypes.EVENT_KMS_KEK_ROTATE;
}
@Override
public String getEventDescription() {
return "Rotating KMS key: " + _uuidMgr.getUuid(KMSKeyResponse.class, id);
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.KmsKey;
}
@Override
public Long getApiResourceId() {
return id;
}
}

View File

@ -47,7 +47,6 @@ import javax.inject.Inject;
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
public class CreateKMSKeyCmd extends BaseCmd implements UserCmd {
private static final String s_name = "createkmskeyresponse";
@Inject
private KMSManager kmsManager;
@ -70,7 +69,7 @@ public class CreateKMSKeyCmd extends BaseCmd implements UserCmd {
@Parameter(name = ApiConstants.PURPOSE,
required = true,
type = CommandType.STRING,
description = "Purpose of the key: VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET")
description = "Purpose of the key: volume, tls")
private String purpose;
@Parameter(name = ApiConstants.ZONE_ID,
@ -144,11 +143,6 @@ public class CreateKMSKeyCmd extends BaseCmd implements UserCmd {
}
}
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
Account caller = CallContext.current().getCallingAccount();
@ -163,4 +157,3 @@ public class CreateKMSKeyCmd extends BaseCmd implements UserCmd {
return ApiCommandResourceType.KmsKey;
}
}

View File

@ -45,7 +45,6 @@ import javax.inject.Inject;
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
public class DeleteKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
private static final String s_name = "deletekmskeyresponse";
@Inject
private KMSManager kmsManager;
@ -85,11 +84,6 @@ public class DeleteKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
}
}
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccount().getId();
@ -110,4 +104,3 @@ public class DeleteKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
return ApiCommandResourceType.KmsKey;
}
}

View File

@ -59,7 +59,7 @@ public class ListKMSKeysCmd extends BaseListAccountResourcesCmd implements UserC
@Parameter(name = ApiConstants.PURPOSE,
type = CommandType.STRING,
description = "Filter by purpose: VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET")
description = "Filter by purpose: volume, tls")
private String purpose;
@Parameter(name = ApiConstants.ZONE_ID,
@ -109,4 +109,3 @@ public class ListKMSKeysCmd extends BaseListAccountResourcesCmd implements UserC
return s_name;
}
}

View File

@ -44,7 +44,6 @@ import javax.inject.Inject;
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
public class UpdateKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
private static final String s_name = "updatekmskeyresponse";
@Inject
private KMSManager kmsManager;
@ -111,11 +110,6 @@ public class UpdateKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
}
}
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccount().getId();

View File

@ -30,6 +30,7 @@ import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.user.UserCmd;
import org.apache.cloudstack.api.response.DiskOfferingResponse;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.api.response.ProjectResponse;
import org.apache.cloudstack.api.response.SnapshotResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
@ -109,6 +110,13 @@ public class CreateVolumeCmd extends BaseAsyncCreateCustomIdCmd implements UserC
description = "The ID of the Instance; to be used with snapshot Id, Instance to which the volume gets attached after creation")
private Long virtualMachineId;
@Parameter(name = ApiConstants.KMS_KEY_ID,
type = CommandType.UUID,
entityType = KMSKeyResponse.class,
description = "ID of the KMS Key for volume encryption (required if encryption enabled for zone)",
since = "4.23.0")
private Long kmsKeyId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -169,6 +177,10 @@ public class CreateVolumeCmd extends BaseAsyncCreateCustomIdCmd implements UserC
return virtualMachineId;
}
public Long getKmsKeyId() {
return kmsKeyId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////

View File

@ -45,7 +45,7 @@ public class KMSKeyResponse extends BaseResponse implements ControlledEntityResp
private String description;
@SerializedName(ApiConstants.PURPOSE)
@Param(description = "the purpose of the key (VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET)")
@Param(description = "the purpose of the key (VOLUME_ENCRYPTION, TLS_CERT)")
private String purpose;
@SerializedName(ApiConstants.ACCOUNT)
@ -253,4 +253,3 @@ public class KMSKeyResponse extends BaseResponse implements ControlledEntityResp
this.kekLabel = kekLabel;
}
}

View File

@ -101,4 +101,3 @@ public interface KMSKey extends Identity, InternalIdentity, ControlledEntity {
Deleted
}
}

View File

@ -18,6 +18,8 @@
package org.apache.cloudstack.kms;
import com.cloud.utils.component.Manager;
import org.apache.cloudstack.api.command.admin.kms.MigrateVolumesToKMSCmd;
import org.apache.cloudstack.api.command.admin.kms.RotateKMSKeyCmd;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.api.command.user.kms.CreateKMSKeyCmd;
@ -288,30 +290,8 @@ public interface KMSManager extends Manager, Configurable {
* @param keyUuid the key UUID
* @return true if caller has permission
*/
boolean hasPermission(Long callerAccountId, String keyUuid);
boolean hasPermission(Long callerAccountId, KMSKey key);
/**
* Delete a KMS key (only if not in use)
*
* @param uuid the key UUID
* @param callerAccountId the caller's account ID
* @throws KMSException if deletion fails (e.g., key in use)
*/
void deleteUserKMSKey(String uuid, Long callerAccountId) throws KMSException;
/**
* Update a KMS key's metadata (name, description, state)
*
* @param uuid the key UUID
* @param callerAccountId the caller's account ID
* @param name optional new name
* @param description optional new description
* @param state optional new state
* @return the updated KMS key
* @throws KMSException if update fails
*/
KMSKey updateUserKMSKey(String uuid, Long callerAccountId,
String name, String description, KMSKey.State state) throws KMSException;
/**
* Unwrap a DEK by wrapped key ID, trying multiple KEK versions if needed
@ -330,7 +310,7 @@ public interface KMSManager extends Manager, Configurable {
* @return wrapped key ready for database storage
* @throws KMSException if operation fails
*/
WrappedKey generateVolumeKeyWithKek(String kekUuid, Long callerAccountId) throws KMSException;
WrappedKey generateVolumeKeyWithKek(KMSKey kmsKey, Long callerAccountId) throws KMSException;
// ==================== API Response Methods ====================
@ -372,4 +352,35 @@ public interface KMSManager extends Manager, Configurable {
* @throws KMSException if deletion fails
*/
SuccessResponse deleteKMSKey(DeleteKMSKeyCmd cmd) throws KMSException;
// ==================== Admin Operations ====================
/**
* Rotate KEK by creating new version and scheduling gradual re-encryption
*
* @param cmd the rotate command with all parameters
* @return New KEK version UUID
* @throws KMSException if rotation fails
*/
String rotateKMSKey(RotateKMSKeyCmd cmd) throws KMSException;
/**
* Gradually rewrap all wrapped keys for a KMS key to use new KEK version
*
* @param kmsKeyId KMS key ID
* @param newKekVersionId New active KEK version ID
* @param batchSize Number of keys to process per batch
* @return Number of keys successfully rewrapped
* @throws KMSException if rewrap fails
*/
int rewrapWrappedKeysForKMSKey(Long kmsKeyId, Long newKekVersionId, int batchSize) throws KMSException;
/**
* Migrate passphrase-based volumes to KMS encryption
*
* @param cmd the migrate command with all parameters
* @return Number of volumes successfully migrated
* @throws KMSException if migration fails
*/
int migrateVolumesToKMS(MigrateVolumesToKMSCmd cmd) throws KMSException;
}

View File

@ -251,6 +251,11 @@
<artifactId>cloud-plugin-metrics</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-kms-database</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-network-nvp</artifactId>

View File

@ -0,0 +1,21 @@
#
# 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=kms
parent=core

View File

@ -0,0 +1,29 @@
<!--
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.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
>
<bean class="org.apache.cloudstack.spring.lifecycle.registry.RegistryLifecycle">
<property name="registry" ref="kmsProvidersRegistry" />
<property name="typeClass" value="org.apache.cloudstack.framework.kms.KMSProvider" />
</bean>
</beans>

View File

@ -85,6 +85,13 @@ import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao;
import org.apache.cloudstack.secret.PassphraseVO;
import org.apache.cloudstack.secret.dao.PassphraseDao;
import org.apache.cloudstack.snapshot.SnapshotHelper;
import org.apache.cloudstack.kms.KMSManager;
import org.apache.cloudstack.kms.KMSKeyVO;
import org.apache.cloudstack.kms.KMSWrappedKeyVO;
import org.apache.cloudstack.kms.dao.KMSKeyDao;
import org.apache.cloudstack.kms.dao.KMSWrappedKeyDao;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.framework.kms.WrappedKey;
import org.apache.cloudstack.storage.command.CommandResult;
import org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
@ -279,6 +286,12 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
@Inject
private DataStoreProviderManager dataStoreProviderMgr;
@Inject
private KMSManager kmsManager;
@Inject
private KMSKeyDao kmsKeyDao;
@Inject
private KMSWrappedKeyDao kmsWrappedKeyDao;
private final StateMachine2<Volume.State, Volume.Event, Volume> _volStateMachine;
protected List<StoragePoolAllocator> _storagePoolAllocators;
@ -507,7 +520,9 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
DiskOffering diskOffering = _entityMgr.findById(DiskOffering.class, volume.getDiskOfferingId());
if (diskOffering.getEncrypt()) {
VolumeVO vol = (VolumeVO) volume;
volume = setPassphraseForVolumeEncryption(vol);
// Retrieve KMS key from volume's kmsKeyId if provided
KMSKeyVO kmsKey = getKmsKeyFromVolume(vol);
volume = setPassphraseForVolumeEncryption(vol, kmsKey, volume.getAccountId());
}
DataCenter dc = _entityMgr.findById(DataCenter.class, volume.getDataCenterId());
DiskProfile dskCh = new DiskProfile(volume, diskOffering, snapshot.getHypervisorType());
@ -724,7 +739,9 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
if (diskOffering.getEncrypt()) {
VolumeVO vol = _volsDao.findById(volumeInfo.getId());
setPassphraseForVolumeEncryption(vol);
// Retrieve KMS key from volume's kmsKeyId if provided
KMSKeyVO kmsKey = getKmsKeyFromVolume(vol);
setPassphraseForVolumeEncryption(vol, kmsKey, vol.getAccountId());
volumeInfo = volFactory.getVolume(volumeInfo.getId());
}
}
@ -1775,7 +1792,9 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
if (vol.getState() == Volume.State.Allocated || vol.getState() == Volume.State.Creating) {
DiskOffering diskOffering = _entityMgr.findById(DiskOffering.class, vol.getDiskOfferingId());
if (diskOffering.getEncrypt()) {
vol = setPassphraseForVolumeEncryption(vol);
// Retrieve KMS key from volume's kmsKeyId if provided
KMSKeyVO kmsKey = getKmsKeyFromVolume(vol);
vol = setPassphraseForVolumeEncryption(vol, kmsKey, vol.getAccountId());
}
newVol = vol;
} else {
@ -1898,10 +1917,68 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
return new Pair<>(newVol, destPool);
}
/**
* Helper method to retrieve KMS key from volume's kmsKeyId
*/
private KMSKeyVO getKmsKeyFromVolume(VolumeVO volume) {
if (volume.getKmsKeyId() == null) {
return null;
}
return kmsKeyDao.findById(volume.getKmsKeyId());
}
private VolumeVO setPassphraseForVolumeEncryption(VolumeVO volume) {
if (volume.getPassphraseId() != null) {
return setPassphraseForVolumeEncryption(volume, null, null);
}
private VolumeVO setPassphraseForVolumeEncryption(VolumeVO volume, KMSKeyVO kmsKey, Long callerAccountId) {
// If volume already has encryption set up, return it
if (volume.getKmsWrappedKeyId() != null || volume.getPassphraseId() != null) {
return volume;
}
Long zoneId = volume.getDataCenterId();
// Check if KMS is enabled for zone AND KMS key is provided
if (kmsManager != null && kmsManager.isKmsEnabled(zoneId) && kmsKey != null) {
// Determine caller account ID if not provided
if (callerAccountId == null) {
callerAccountId = volume.getAccountId();
}
// Validate permission
if (!kmsManager.hasPermission(callerAccountId, kmsKey)) {
throw new CloudRuntimeException("No permission to use KMS key: " + kmsKey);
}
try {
logger.debug("Generating and wrapping DEK for volume {} using KMS key {}", volume.getName(), kmsKey.getUuid());
long startTime = System.currentTimeMillis();
// Generate and wrap DEK using active KEK version
WrappedKey wrappedKey = kmsManager.generateVolumeKeyWithKek(kmsKey, callerAccountId);
// The wrapped key is already persisted by generateVolumeKeyWithKek, get its ID
KMSWrappedKeyVO wrappedKeyVO = kmsWrappedKeyDao.findByUuid(wrappedKey.getUuid());
if (wrappedKeyVO == null) {
throw new CloudRuntimeException("Failed to find persisted wrapped key: " + wrappedKey.getUuid());
}
// Set the wrapped key ID on the volume
volume.setKmsWrappedKeyId(wrappedKeyVO.getId());
long finishTime = System.currentTimeMillis();
logger.debug("Generating and persisting wrapped key took {} ms for volume: {}",
(finishTime - startTime), volume.getName());
return _volsDao.persist(volume);
} catch (KMSException e) {
throw new CloudRuntimeException("KMS failure while setting up volume encryption: " + e.getMessage(), e);
}
}
// Legacy: passphrase-based encryption (fallback when KMS not enabled or KMS key not specified)
logger.debug("Creating passphrase for the volume: " + volume.getName());
long startTime = System.currentTimeMillis();
PassphraseVO passphrase = passphraseDao.persist(new PassphraseVO(true));

View File

@ -182,6 +182,9 @@ public class VolumeVO implements Volume {
@Column(name = "passphrase_id")
private Long passphraseId;
@Column(name = "kms_key_id")
private Long kmsKeyId;
@Column(name = "kms_wrapped_key_id")
private Long kmsWrappedKeyId;
@ -686,6 +689,10 @@ public class VolumeVO implements Volume {
public void setPassphraseId(Long id) { this.passphraseId = id; }
public Long getKmsKeyId() { return kmsKeyId; }
public void setKmsKeyId(Long id) { this.kmsKeyId = id; }
public Long getKmsWrappedKeyId() { return kmsWrappedKeyId; }
public void setKmsWrappedKeyId(Long id) { this.kmsWrappedKeyId = id; }

View File

@ -109,6 +109,17 @@ public interface VolumeDao extends GenericDao<VolumeVO, Long>, StateDao<Volume.S
*/
List<VolumeVO> listVolumesByPassphraseId(long passphraseId);
/**
* List volumes with passphrase_id for migration to KMS
*
* @param zoneId Zone ID (required)
* @param accountId Account ID filter (optional, null for all accounts)
* @param domainId Domain ID filter (optional, null for all domains)
* @param limit Maximum number of volumes to return
* @return list of volumes that need migration
*/
Pair<List<VolumeVO>, Integer> listVolumesForKMSMigration(Long zoneId, Long accountId, Long domainId, Integer limit);
/**
* Gets the Total Primary Storage space allocated for an account
*

View File

@ -79,6 +79,7 @@ public class VolumeDaoImpl extends GenericDaoBase<VolumeVO, Long> implements Vol
protected GenericSearchBuilder<VolumeVO, SumCount> secondaryStorageSearch;
private final SearchBuilder<VolumeVO> poolAndPathSearch;
final GenericSearchBuilder<VolumeVO, Integer> CountByOfferingId;
private final SearchBuilder<VolumeVO> kmsMigrationSearch;
@Inject
ReservationDao reservationDao;
@ -512,6 +513,13 @@ public class VolumeDaoImpl extends GenericDaoBase<VolumeVO, Long> implements Vol
CountByOfferingId.select(null, Func.COUNT, CountByOfferingId.entity().getId());
CountByOfferingId.and("diskOfferingId", CountByOfferingId.entity().getDiskOfferingId(), Op.EQ);
CountByOfferingId.done();
kmsMigrationSearch = createSearchBuilder();
kmsMigrationSearch.and("passphraseId", kmsMigrationSearch.entity().getPassphraseId(), Op.NNULL);
kmsMigrationSearch.and("zoneId", kmsMigrationSearch.entity().getDataCenterId(), Op.EQ);
kmsMigrationSearch.and("accountId", kmsMigrationSearch.entity().getAccountId(), Op.EQ);
kmsMigrationSearch.and("domainId", kmsMigrationSearch.entity().getDomainId(), Op.EQ);
kmsMigrationSearch.done();
}
@Override
@ -732,6 +740,23 @@ public class VolumeDaoImpl extends GenericDaoBase<VolumeVO, Long> implements Vol
return listBy(sc);
}
@Override
public Pair<List<VolumeVO>, Integer> listVolumesForKMSMigration(Long zoneId, Long accountId, Long domainId, Integer limit) {
SearchCriteria<VolumeVO> sc = kmsMigrationSearch.create();
Filter filter = new Filter(limit);
sc.setParameters("zoneId", zoneId);
if (accountId != null) {
sc.setParameters("accountId", accountId);
}
if (domainId != null) {
sc.setParameters("domainId", domainId);
}
Integer count = getCount(sc);
List<VolumeVO> volumes = listBy(sc, filter);
return new Pair<>(volumes, count);
}
@Override
@DB
public boolean remove(Long id) {

View File

@ -18,6 +18,7 @@
package org.apache.cloudstack.kms;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import javax.persistence.Column;
import javax.persistence.Entity;
@ -89,9 +90,6 @@ public class KMSKekVersionVO {
Archived
}
/**
* Default constructor (required by JPA)
*/
public KMSKekVersionVO() {
this.uuid = UUID.randomUUID().toString();
this.created = new Date();
@ -114,8 +112,6 @@ public class KMSKekVersionVO {
this.status = status;
}
// Getters and Setters
public Long getId() {
return id;
}
@ -182,8 +178,8 @@ public class KMSKekVersionVO {
@Override
public String toString() {
return String.format("KMSKekVersion[id=%d, uuid=%s, kmsKeyId=%d, version=%d, status=%s, kekLabel=%s]",
id, uuid, kmsKeyId, versionNumber, status, kekLabel);
return String.format("KMSKekVersion %s",
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(
this, "id", "uuid", "kmsKeyId", "versionNumber", "status", "kekLabel"));
}
}

View File

@ -19,6 +19,7 @@ package org.apache.cloudstack.kms;
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;
@ -92,18 +93,12 @@ public class KMSKeyVO implements KMSKey {
@Temporal(TemporalType.TIMESTAMP)
private Date removed;
/**
* Default constructor (required by JPA)
*/
public KMSKeyVO() {
this.uuid = UUID.randomUUID().toString();
this.created = new Date();
this.state = State.Enabled;
}
/**
* Constructor for creating a new KMS key
*/
public KMSKeyVO(String name, String description, String kekLabel, KeyPurpose purpose,
Long accountId, Long domainId, Long zoneId, String providerName,
String algorithm, Integer keyBits) {
@ -120,8 +115,6 @@ public class KMSKeyVO implements KMSKey {
this.keyBits = keyBits;
}
// Identity interface methods
@Override
public long getId() {
return id;
@ -132,8 +125,6 @@ public class KMSKeyVO implements KMSKey {
return uuid;
}
// KMSKey interface methods
@Override
public String getName() {
return name;
@ -206,8 +197,6 @@ public class KMSKeyVO implements KMSKey {
return KMSKey.class;
}
// Setters
public void setId(Long id) {
this.id = id;
}
@ -270,8 +259,7 @@ public class KMSKeyVO implements KMSKey {
@Override
public String toString() {
return String.format("KMSKey[id=%d, uuid=%s, name=%s, purpose=%s, account=%d, zone=%d, state=%s]",
id, uuid, name, purpose, accountId, zoneId, state);
return String.format("KMSKey %s",
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "name", "purpose", "accountId", "zoneId", "state"));
}
}

View File

@ -18,6 +18,7 @@
package org.apache.cloudstack.kms;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import javax.persistence.Column;
import javax.persistence.Entity;
@ -68,20 +69,13 @@ public class KMSWrappedKeyVO {
@Temporal(TemporalType.TIMESTAMP)
private Date removed;
/**
* Constructor for creating a new wrapped key entry
*/
public KMSWrappedKeyVO(KMSKeyVO kmsKey, byte[] wrappedBlob) {
this();
this.kmsKeyId = kmsKey.getId();
this.zoneId = kmsKey.getZoneId();
// Defensive copy
this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null;
}
/**
* Constructor for creating a new wrapped key entry with KEK version
*/
public KMSWrappedKeyVO(KMSKeyVO kmsKey, Long kekVersionId, byte[] wrappedBlob) {
this();
this.kmsKeyId = kmsKey.getId();
@ -91,39 +85,26 @@ public class KMSWrappedKeyVO {
this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null;
}
/**
* Constructor with explicit parameters
*/
public KMSWrappedKeyVO(Long kmsKeyId, Long zoneId, byte[] wrappedBlob) {
this();
this.kmsKeyId = kmsKeyId;
this.zoneId = zoneId;
// Defensive copy
this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null;
}
/**
* Constructor with explicit parameters including KEK version
*/
public KMSWrappedKeyVO(Long kmsKeyId, Long kekVersionId, Long zoneId, byte[] wrappedBlob) {
this();
this.kmsKeyId = kmsKeyId;
this.kekVersionId = kekVersionId;
this.zoneId = zoneId;
// Defensive copy
this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null;
}
/**
* Default constructor (required by JPA)
*/
public KMSWrappedKeyVO() {
this.uuid = UUID.randomUUID().toString();
this.created = new Date();
}
// Getters and Setters
public Long getId() {
return id;
}
@ -165,12 +146,10 @@ public class KMSWrappedKeyVO {
}
public byte[] getWrappedBlob() {
// Return defensive copy
return wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null;
}
public void setWrappedBlob(byte[] wrappedBlob) {
// Store defensive copy
this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null;
}
@ -192,16 +171,8 @@ public class KMSWrappedKeyVO {
@Override
public String toString() {
return "KMSWrappedKeyVO{" +
"id=" + id +
", uuid='" + uuid + '\'' +
", kmsKeyId=" + kmsKeyId +
", kekVersionId=" + kekVersionId +
", zoneId=" + zoneId +
", blobSize=" + (wrappedBlob != null ? wrappedBlob.length : 0) +
", created=" + created +
", removed=" + removed +
'}';
return String.format("KMSWrappedKey %s",
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(
this, "id", "uuid", "kmsKeyId", "kekVersionId", "accountId", "zoneId", "state", "created", "removed"));
}
}

View File

@ -22,16 +22,7 @@ import org.apache.cloudstack.kms.KMSKekVersionVO;
import java.util.List;
/**
* DAO for KMSKekVersion entities
*/
public interface KMSKekVersionDao extends GenericDao<KMSKekVersionVO, Long> {
/**
* Find a KEK version by UUID
*/
KMSKekVersionVO findByUuid(String uuid);
/**
* Get the active version for a KMS key
*/
@ -57,4 +48,3 @@ public interface KMSKekVersionDao extends GenericDao<KMSKekVersionVO, Long> {
*/
KMSKekVersionVO findByKekLabel(String kekLabel);
}

View File

@ -25,72 +25,23 @@ import org.springframework.stereotype.Component;
import java.util.List;
/**
* Implementation of KMSKekVersionDao
*/
@Component
public class KMSKekVersionDaoImpl extends GenericDaoBase<KMSKekVersionVO, Long> implements KMSKekVersionDao {
private final SearchBuilder<KMSKekVersionVO> uuidSearch;
private final SearchBuilder<KMSKekVersionVO> kmsKeyIdSearch;
private final SearchBuilder<KMSKekVersionVO> activeVersionSearch;
private final SearchBuilder<KMSKekVersionVO> decryptionVersionsSearch;
private final SearchBuilder<KMSKekVersionVO> versionNumberSearch;
private final SearchBuilder<KMSKekVersionVO> kekLabelSearch;
private final SearchBuilder<KMSKekVersionVO> allFieldSearch;
public KMSKekVersionDaoImpl() {
super();
// Search by UUID
uuidSearch = createSearchBuilder();
uuidSearch.and("uuid", uuidSearch.entity().getUuid(), SearchCriteria.Op.EQ);
uuidSearch.and("removed", uuidSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
uuidSearch.done();
// Search by KMS key ID
kmsKeyIdSearch = createSearchBuilder();
kmsKeyIdSearch.and("kmsKeyId", kmsKeyIdSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
kmsKeyIdSearch.and("removed", kmsKeyIdSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
kmsKeyIdSearch.done();
// Search for active version by KMS key ID
activeVersionSearch = createSearchBuilder();
activeVersionSearch.and("kmsKeyId", activeVersionSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
activeVersionSearch.and("status", activeVersionSearch.entity().getStatus(), SearchCriteria.Op.EQ);
activeVersionSearch.and("removed", activeVersionSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
activeVersionSearch.done();
// Search for versions usable for decryption (Active or Previous)
decryptionVersionsSearch = createSearchBuilder();
decryptionVersionsSearch.and("kmsKeyId", decryptionVersionsSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
decryptionVersionsSearch.and("status", decryptionVersionsSearch.entity().getStatus(), SearchCriteria.Op.IN);
decryptionVersionsSearch.and("removed", decryptionVersionsSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
decryptionVersionsSearch.done();
// Search by KMS key ID and version number
versionNumberSearch = createSearchBuilder();
versionNumberSearch.and("kmsKeyId", versionNumberSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
versionNumberSearch.and("versionNumber", versionNumberSearch.entity().getVersionNumber(), SearchCriteria.Op.EQ);
versionNumberSearch.and("removed", versionNumberSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
versionNumberSearch.done();
// Search by KEK label
kekLabelSearch = createSearchBuilder();
kekLabelSearch.and("kekLabel", kekLabelSearch.entity().getKekLabel(), SearchCriteria.Op.EQ);
kekLabelSearch.and("removed", kekLabelSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
kekLabelSearch.done();
}
@Override
public KMSKekVersionVO findByUuid(String uuid) {
SearchCriteria<KMSKekVersionVO> sc = uuidSearch.create();
sc.setParameters("uuid", uuid);
return findOneBy(sc);
allFieldSearch = createSearchBuilder();
allFieldSearch.and("kmsKeyId", allFieldSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
allFieldSearch.and("status", allFieldSearch.entity().getStatus(), SearchCriteria.Op.IN);
allFieldSearch.and("versionNumber", allFieldSearch.entity().getVersionNumber(), SearchCriteria.Op.EQ);
allFieldSearch.and("kekLabel", allFieldSearch.entity().getKekLabel(), SearchCriteria.Op.EQ);
allFieldSearch.done();
}
@Override
public KMSKekVersionVO getActiveVersion(Long kmsKeyId) {
SearchCriteria<KMSKekVersionVO> sc = activeVersionSearch.create();
SearchCriteria<KMSKekVersionVO> sc = allFieldSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
sc.setParameters("status", KMSKekVersionVO.Status.Active);
return findOneBy(sc);
@ -98,7 +49,7 @@ public class KMSKekVersionDaoImpl extends GenericDaoBase<KMSKekVersionVO, Long>
@Override
public List<KMSKekVersionVO> getVersionsForDecryption(Long kmsKeyId) {
SearchCriteria<KMSKekVersionVO> sc = decryptionVersionsSearch.create();
SearchCriteria<KMSKekVersionVO> sc = allFieldSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
sc.setParameters("status", KMSKekVersionVO.Status.Active, KMSKekVersionVO.Status.Previous);
return listBy(sc);
@ -106,14 +57,14 @@ public class KMSKekVersionDaoImpl extends GenericDaoBase<KMSKekVersionVO, Long>
@Override
public List<KMSKekVersionVO> listByKmsKeyId(Long kmsKeyId) {
SearchCriteria<KMSKekVersionVO> sc = kmsKeyIdSearch.create();
SearchCriteria<KMSKekVersionVO> sc = allFieldSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
return listBy(sc);
}
@Override
public KMSKekVersionVO findByKmsKeyIdAndVersion(Long kmsKeyId, Integer versionNumber) {
SearchCriteria<KMSKekVersionVO> sc = versionNumberSearch.create();
SearchCriteria<KMSKekVersionVO> sc = allFieldSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
sc.setParameters("versionNumber", versionNumber);
return findOneBy(sc);
@ -121,9 +72,8 @@ public class KMSKekVersionDaoImpl extends GenericDaoBase<KMSKekVersionVO, Long>
@Override
public KMSKekVersionVO findByKekLabel(String kekLabel) {
SearchCriteria<KMSKekVersionVO> sc = kekLabelSearch.create();
SearchCriteria<KMSKekVersionVO> sc = allFieldSearch.create();
sc.setParameters("kekLabel", kekLabel);
return findOneBy(sc);
}
}

View File

@ -24,16 +24,8 @@ import org.apache.cloudstack.kms.KMSKeyVO;
import java.util.List;
/**
* DAO for KMSKey entities
*/
public interface KMSKeyDao extends GenericDao<KMSKeyVO, Long> {
/**
* Find a KMS key by UUID
*/
KMSKeyVO findByUuid(String uuid);
/**
* Find a KMS key by KEK label and provider
*/
@ -44,11 +36,6 @@ public interface KMSKeyDao extends GenericDao<KMSKeyVO, Long> {
*/
List<KMSKeyVO> listByAccount(Long accountId, KeyPurpose purpose, KMSKey.State state);
/**
* List KMS keys in a domain (optionally including subdomains)
*/
List<KMSKeyVO> listByDomain(Long domainId, KeyPurpose purpose, KMSKey.State state, boolean includeSubdomains);
/**
* List KMS keys in a zone
*/
@ -69,4 +56,3 @@ public interface KMSKeyDao extends GenericDao<KMSKeyVO, Long> {
*/
long countByKekLabel(String kekLabel, String providerName);
}

View File

@ -28,83 +28,29 @@ import org.springframework.stereotype.Component;
import javax.inject.Inject;
import java.util.List;
/**
* Implementation of KMSKeyDao
*/
@Component
public class KMSKeyDaoImpl extends GenericDaoBase<KMSKeyVO, Long> implements KMSKeyDao {
private final SearchBuilder<KMSKeyVO> uuidSearch;
private final SearchBuilder<KMSKeyVO> kekLabelSearch;
private final SearchBuilder<KMSKeyVO> accountSearch;
private final SearchBuilder<KMSKeyVO> domainSearch;
private final SearchBuilder<KMSKeyVO> zoneSearch;
private final SearchBuilder<KMSKeyVO> accessibleSearch;
private final SearchBuilder<KMSKeyVO> allFieldSearch;
@Inject
private KMSWrappedKeyDao kmsWrappedKeyDao;
public KMSKeyDaoImpl() {
super();
// Search by UUID
uuidSearch = createSearchBuilder();
uuidSearch.and("uuid", uuidSearch.entity().getUuid(), SearchCriteria.Op.EQ);
uuidSearch.and("removed", uuidSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
uuidSearch.done();
// Search by KEK label and provider
kekLabelSearch = createSearchBuilder();
kekLabelSearch.and("kekLabel", kekLabelSearch.entity().getKekLabel(), SearchCriteria.Op.EQ);
kekLabelSearch.and("providerName", kekLabelSearch.entity().getProviderName(), SearchCriteria.Op.EQ);
kekLabelSearch.and("removed", kekLabelSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
kekLabelSearch.done();
// Search by account
accountSearch = createSearchBuilder();
accountSearch.and("accountId", accountSearch.entity().getAccountId(), SearchCriteria.Op.EQ);
accountSearch.and("purpose", accountSearch.entity().getPurpose(), SearchCriteria.Op.EQ);
accountSearch.and("state", accountSearch.entity().getState(), SearchCriteria.Op.EQ);
accountSearch.and("removed", accountSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
accountSearch.done();
// Search by domain
domainSearch = createSearchBuilder();
domainSearch.and("domainId", domainSearch.entity().getDomainId(), SearchCriteria.Op.EQ);
domainSearch.and("purpose", domainSearch.entity().getPurpose(), SearchCriteria.Op.EQ);
domainSearch.and("state", domainSearch.entity().getState(), SearchCriteria.Op.EQ);
domainSearch.and("removed", domainSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
domainSearch.done();
// Search by zone
zoneSearch = createSearchBuilder();
zoneSearch.and("zoneId", zoneSearch.entity().getZoneId(), SearchCriteria.Op.EQ);
zoneSearch.and("purpose", zoneSearch.entity().getPurpose(), SearchCriteria.Op.EQ);
zoneSearch.and("state", zoneSearch.entity().getState(), SearchCriteria.Op.EQ);
zoneSearch.and("removed", zoneSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
zoneSearch.done();
// Search for accessible keys (by account or domain)
accessibleSearch = createSearchBuilder();
accessibleSearch.and("accountId", accessibleSearch.entity().getAccountId(), SearchCriteria.Op.EQ);
accessibleSearch.and("domainId", accessibleSearch.entity().getDomainId(), SearchCriteria.Op.EQ);
accessibleSearch.and("zoneId", accessibleSearch.entity().getZoneId(), SearchCriteria.Op.EQ);
accessibleSearch.and("purpose", accessibleSearch.entity().getPurpose(), SearchCriteria.Op.EQ);
accessibleSearch.and("state", accessibleSearch.entity().getState(), SearchCriteria.Op.EQ);
accessibleSearch.and("removed", accessibleSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
accessibleSearch.done();
}
@Override
public KMSKeyVO findByUuid(String uuid) {
SearchCriteria<KMSKeyVO> sc = uuidSearch.create();
sc.setParameters("uuid", uuid);
return findOneBy(sc);
allFieldSearch = createSearchBuilder();
allFieldSearch.and("kekLabel", allFieldSearch.entity().getKekLabel(), SearchCriteria.Op.EQ);
allFieldSearch.and("providerName", allFieldSearch.entity().getProviderName(), SearchCriteria.Op.EQ);
allFieldSearch.and("domainId", allFieldSearch.entity().getDomainId(), SearchCriteria.Op.EQ);
allFieldSearch.and("accountId", allFieldSearch.entity().getAccountId(), SearchCriteria.Op.EQ);
allFieldSearch.and("purpose", allFieldSearch.entity().getPurpose(), SearchCriteria.Op.EQ);
allFieldSearch.and("state", allFieldSearch.entity().getState(), SearchCriteria.Op.EQ);
allFieldSearch.and("zoneId", allFieldSearch.entity().getZoneId(), SearchCriteria.Op.EQ);
allFieldSearch.done();
}
@Override
public KMSKeyVO findByKekLabel(String kekLabel, String providerName) {
SearchCriteria<KMSKeyVO> sc = kekLabelSearch.create();
SearchCriteria<KMSKeyVO> sc = allFieldSearch.create();
sc.setParameters("kekLabel", kekLabel);
sc.setParameters("providerName", providerName);
return findOneBy(sc);
@ -112,7 +58,7 @@ public class KMSKeyDaoImpl extends GenericDaoBase<KMSKeyVO, Long> implements KMS
@Override
public List<KMSKeyVO> listByAccount(Long accountId, KeyPurpose purpose, KMSKey.State state) {
SearchCriteria<KMSKeyVO> sc = accountSearch.create();
SearchCriteria<KMSKeyVO> sc = allFieldSearch.create();
sc.setParameters("accountId", accountId);
if (purpose != null) {
sc.setParameters("purpose", purpose);
@ -123,24 +69,9 @@ public class KMSKeyDaoImpl extends GenericDaoBase<KMSKeyVO, Long> implements KMS
return listBy(sc);
}
@Override
public List<KMSKeyVO> listByDomain(Long domainId, KeyPurpose purpose, KMSKey.State state, boolean includeSubdomains) {
SearchCriteria<KMSKeyVO> sc = domainSearch.create();
sc.setParameters("domainId", domainId);
if (purpose != null) {
sc.setParameters("purpose", purpose);
}
if (state != null) {
sc.setParameters("state", state);
}
// TODO: Implement subdomain traversal if includeSubdomains is true
// For now, just return keys in this domain
return listBy(sc);
}
@Override
public List<KMSKeyVO> listByZone(Long zoneId, KeyPurpose purpose, KMSKey.State state) {
SearchCriteria<KMSKeyVO> sc = zoneSearch.create();
SearchCriteria<KMSKeyVO> sc = allFieldSearch.create();
sc.setParameters("zoneId", zoneId);
if (purpose != null) {
sc.setParameters("purpose", purpose);
@ -153,8 +84,7 @@ public class KMSKeyDaoImpl extends GenericDaoBase<KMSKeyVO, Long> implements KMS
@Override
public List<KMSKeyVO> listAccessibleKeys(Long accountId, Long domainId, Long zoneId, KeyPurpose purpose, KMSKey.State state) {
SearchCriteria<KMSKeyVO> sc = accessibleSearch.create();
// Keys owned by the account or in the domain
SearchCriteria<KMSKeyVO> sc = allFieldSearch.create();
sc.setParameters("accountId", accountId);
if (zoneId != null) {
sc.setParameters("zoneId", zoneId);
@ -173,17 +103,15 @@ public class KMSKeyDaoImpl extends GenericDaoBase<KMSKeyVO, Long> implements KMS
if (kmsKeyId == null) {
return 0;
}
// Delegate to KMSWrappedKeyDao
return kmsWrappedKeyDao.countByKmsKeyId(kmsKeyId);
}
@Override
public long countByKekLabel(String kekLabel, String providerName) {
SearchCriteria<KMSKeyVO> sc = kekLabelSearch.create();
SearchCriteria<KMSKeyVO> sc = allFieldSearch.create();
sc.setParameters("kekLabel", kekLabel);
sc.setParameters("providerName", providerName);
Integer count = getCount(sc);
return count != null ? count.longValue() : 0L;
}
}

View File

@ -29,14 +29,6 @@ import java.util.List;
*/
public interface KMSWrappedKeyDao extends GenericDao<KMSWrappedKeyVO, Long> {
/**
* Find a wrapped key by UUID
*
* @param uuid the key UUID
* @return the wrapped key, or null if not found
*/
KMSWrappedKeyVO findByUuid(String uuid);
/**
* List all wrapped keys using a specific KMS key
* (useful for key rotation)
@ -69,5 +61,13 @@ public interface KMSWrappedKeyDao extends GenericDao<KMSWrappedKeyVO, Long> {
* @return list of wrapped keys
*/
List<KMSWrappedKeyVO> listByKekVersionId(Long kekVersionId);
}
/**
* List wrapped keys for a KMS key that need re-encryption (not using specified version)
*
* @param kmsKeyId the KMS key ID
* @param excludeKekVersionId the KEK version ID to exclude (keys using this version don't need rewrap)
* @return list of wrapped keys that need re-encryption
*/
List<KMSWrappedKeyVO> listWrappedKeysForRewrap(long kmsKeyId, long excludeKekVersionId);
}

View File

@ -25,69 +25,50 @@ import org.springframework.stereotype.Component;
import java.util.List;
/**
* Implementation of KMSWrappedKeyDao
*/
@Component
public class KMSWrappedKeyDaoImpl extends GenericDaoBase<KMSWrappedKeyVO, Long> implements KMSWrappedKeyDao {
private final SearchBuilder<KMSWrappedKeyVO> uuidSearch;
private final SearchBuilder<KMSWrappedKeyVO> kmsKeyIdSearch;
private final SearchBuilder<KMSWrappedKeyVO> kekVersionIdSearch;
private final SearchBuilder<KMSWrappedKeyVO> zoneSearch;
private final SearchBuilder<KMSWrappedKeyVO> allFieldSearch;
private final SearchBuilder<KMSWrappedKeyVO> rewrapExcludeVersionSearch;
public KMSWrappedKeyDaoImpl() {
super();
// Search by UUID
uuidSearch = createSearchBuilder();
uuidSearch.and("uuid", uuidSearch.entity().getUuid(), SearchCriteria.Op.EQ);
uuidSearch.and("removed", uuidSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
uuidSearch.done();
allFieldSearch = createSearchBuilder();
allFieldSearch.and("kmsKeyId", allFieldSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
allFieldSearch.and("kekVersionId", allFieldSearch.entity().getKekVersionId(), SearchCriteria.Op.EQ);
allFieldSearch.and("zoneId", allFieldSearch.entity().getZoneId(), SearchCriteria.Op.EQ);
allFieldSearch.and("kmsKeyId", allFieldSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
allFieldSearch.done();
// Search by KMS Key ID (FK to kms_keys)
kmsKeyIdSearch = createSearchBuilder();
kmsKeyIdSearch.and("kmsKeyId", kmsKeyIdSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
kmsKeyIdSearch.and("removed", kmsKeyIdSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
kmsKeyIdSearch.done();
// Search by KEK Version ID (FK to kms_kek_versions)
kekVersionIdSearch = createSearchBuilder();
kekVersionIdSearch.and("kekVersionId", kekVersionIdSearch.entity().getKekVersionId(), SearchCriteria.Op.EQ);
kekVersionIdSearch.and("removed", kekVersionIdSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
kekVersionIdSearch.done();
// Search by zone
zoneSearch = createSearchBuilder();
zoneSearch.and("zoneId", zoneSearch.entity().getZoneId(), SearchCriteria.Op.EQ);
zoneSearch.and("removed", zoneSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
zoneSearch.done();
}
@Override
public KMSWrappedKeyVO findByUuid(String uuid) {
SearchCriteria<KMSWrappedKeyVO> sc = uuidSearch.create();
sc.setParameters("uuid", uuid);
return findOneBy(sc);
// Search builder for excluding specific version using OR condition
rewrapExcludeVersionSearch = createSearchBuilder();
rewrapExcludeVersionSearch.and("kmsKeyId", rewrapExcludeVersionSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
// OR group: (kekVersionId != excludeKekVersionId OR kekVersionId IS NULL)
rewrapExcludeVersionSearch.and().op("kekVersionId", rewrapExcludeVersionSearch.entity().getKekVersionId(), SearchCriteria.Op.NEQ);
rewrapExcludeVersionSearch.or("kekVersionIdNull", rewrapExcludeVersionSearch.entity().getKekVersionId(), SearchCriteria.Op.NULL);
rewrapExcludeVersionSearch.cp();
rewrapExcludeVersionSearch.done();
}
@Override
public List<KMSWrappedKeyVO> listByKmsKeyId(Long kmsKeyId) {
SearchCriteria<KMSWrappedKeyVO> sc = kmsKeyIdSearch.create();
SearchCriteria<KMSWrappedKeyVO> sc = allFieldSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
return listBy(sc);
}
@Override
public List<KMSWrappedKeyVO> listByZone(Long zoneId) {
SearchCriteria<KMSWrappedKeyVO> sc = zoneSearch.create();
SearchCriteria<KMSWrappedKeyVO> sc = allFieldSearch.create();
sc.setParameters("zoneId", zoneId);
return listBy(sc);
}
@Override
public long countByKmsKeyId(Long kmsKeyId) {
SearchCriteria<KMSWrappedKeyVO> sc = kmsKeyIdSearch.create();
SearchCriteria<KMSWrappedKeyVO> sc = allFieldSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
Integer count = getCount(sc);
return count != null ? count.longValue() : 0L;
@ -95,9 +76,16 @@ public class KMSWrappedKeyDaoImpl extends GenericDaoBase<KMSWrappedKeyVO, Long>
@Override
public List<KMSWrappedKeyVO> listByKekVersionId(Long kekVersionId) {
SearchCriteria<KMSWrappedKeyVO> sc = kekVersionIdSearch.create();
SearchCriteria<KMSWrappedKeyVO> sc = allFieldSearch.create();
sc.setParameters("kekVersionId", kekVersionId);
return listBy(sc);
}
}
@Override
public List<KMSWrappedKeyVO> listWrappedKeysForRewrap(long kmsKeyId, long excludeKekVersionId) {
SearchCriteria<KMSWrappedKeyVO> sc = rewrapExcludeVersionSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
sc.setParameters("kekVersionId", excludeKekVersionId);
return listBy(sc);
}
}

View File

@ -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';

View File

@ -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();

View File

@ -176,4 +176,3 @@ public class KMSException extends CloudRuntimeException {
return errorType.isRetryable();
}
}

View File

@ -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;
* <p>
* 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;
}

View File

@ -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.
* <p>
* This facade abstracts provider-specific details and provides zone-aware
* routing, retry logic, and audit logging for KMS operations.
* <p>
* 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<String> 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)
* <p>
* 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;
}

View File

@ -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");
}
}

View File

@ -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 {
'}';
}
}

View File

@ -17,8 +17,8 @@
specific language governing permissions and limitations
under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-plugin-kms-database</artifactId>

View File

@ -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.
* <p>
* 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<Boolean> 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<String, byte[]> 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<String> 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<KMSDatabaseKekObjectVO> 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<String> 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);
}
}

View File

@ -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.
* <p>
* 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"));
}
}

View File

@ -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<KMSDatabaseKekObjectVO, Long> {
/**
* 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<KMSDatabaseKekObjectVO> listByPurpose(KeyPurpose purpose);
/**
* List all KEK objects by key type (PKCS#11 CKA_KEY_TYPE)
*/
List<KMSDatabaseKekObjectVO> listByKeyType(String keyType);
/**
* List all KEK objects by object class (PKCS#11 CKA_CLASS)
*/
List<KMSDatabaseKekObjectVO> listByObjectClass(String objectClass);
/**
* Check if a KEK object exists with the given label
*/
boolean existsByLabel(String label);
}

View File

@ -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<KMSDatabaseKekObjectVO, Long> implements KMSDatabaseKekObjectDao {
private final SearchBuilder<KMSDatabaseKekObjectVO> 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<KMSDatabaseKekObjectVO> sc = allFieldSearch.create();
sc.setParameters("label", label);
return findOneBy(sc);
}
@Override
public KMSDatabaseKekObjectVO findByObjectId(byte[] objectId) {
SearchCriteria<KMSDatabaseKekObjectVO> sc = allFieldSearch.create();
sc.setParameters("objectId", objectId);
return findOneBy(sc);
}
@Override
public List<KMSDatabaseKekObjectVO> listByPurpose(KeyPurpose purpose) {
SearchCriteria<KMSDatabaseKekObjectVO> sc = allFieldSearch.create();
sc.setParameters("purpose", purpose);
return listBy(sc);
}
@Override
public List<KMSDatabaseKekObjectVO> listByKeyType(String keyType) {
SearchCriteria<KMSDatabaseKekObjectVO> sc = allFieldSearch.create();
sc.setParameters("keyType", keyType);
return listBy(sc);
}
@Override
public List<KMSDatabaseKekObjectVO> listByObjectClass(String objectClass) {
SearchCriteria<KMSDatabaseKekObjectVO> sc = allFieldSearch.create();
sc.setParameters("objectClass", objectClass);
return listBy(sc);
}
@Override
public boolean existsByLabel(String label) {
return findByLabel(label) != null;
}
}

View File

@ -16,5 +16,4 @@
# under the License.
name=database-kms
parent=kmsProvidersRegistry
parent=kms

View File

@ -16,21 +16,18 @@
specific language governing permissions and limitations
under the License.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
>
<!-- Database KMS Provider (default provider) -->
<context:component-scan base-package="org.apache.cloudstack.kms.provider.database"/>
<bean id="databaseKMSProvider" class="org.apache.cloudstack.kms.provider.DatabaseKMSProvider">
<property name="name" value="DatabaseKMSProvider" />
<property name="name" value="DatabaseKMSProvider"/>
</bean>
</beans>

View File

@ -17,8 +17,8 @@
specific language governing permissions and limitations
under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>cloudstack-kms-plugins</artifactId>

View File

@ -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;

View File

@ -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<String, String> 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<String, String> details, final Long kmsKeyId) {
return Transaction.execute(new TransactionCallback<VolumeVO>() {
@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);
}

View File

@ -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<String, KMSProvider> 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<KMSProvider> 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<KMSKekVersionVO> versions = getKekVersionsForDecryption(kmsKey.getId());
List<KMSKekVersionVO> 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<KMSKekVersionVO> 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<KMSKeyResponse> listKMSKeys(ListKMSKeysCmd cmd) {
Account caller = CallContext.current().getCallingAccount();
if (caller == null) {
ListResponse<KMSKeyResponse> 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<KMSKeyResponse> responses = new ArrayList<>();
responses.add(responseGenerator.createKMSKeyResponse(key));
ListResponse<KMSKeyResponse> 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<KMSKeyResponse> responses = new java.util.ArrayList<>();
if (kmsKey != null && hasPermission(caller.getId(), kmsKey.getUuid())) {
responses.add(responseGenerator.createKMSKeyResponse(kmsKey));
}
ListResponse<KMSKeyResponse> 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<KMSKeyResponse> responses = new java.util.ArrayList<>();
List<KMSKeyResponse> 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<KMSWrappedKeyVO> 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<KMSWrappedKeyVO> 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<KMSKekVersionVO> oldVersions = kmsKekVersionDao.getVersionsForDecryption(kmsKeyId);
for (KMSKekVersionVO oldVersion : oldVersions) {
if (oldVersion.getStatus() == KMSKekVersionVO.Status.Previous) {
List<KMSWrappedKeyVO> 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<List<VolumeVO>, Integer> volumeListPair = volumeDao.listVolumesForKMSMigration(zoneId, accountId, domainId, pageSize);
List<VolumeVO> 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<KMSKeyResponse> createEmptyListResponse() {
ListResponse<KMSKeyResponse> 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;
}
}