mirror of https://github.com/apache/cloudstack.git
integrate volume encryption with kms
This commit is contained in:
parent
e995b46b20
commit
e3a63ec932
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,4 +101,3 @@ public interface KMSKey extends Identity, InternalIdentity, ControlledEntity {
|
|||
Deleted
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -176,4 +176,3 @@ public class KMSException extends CloudRuntimeException {
|
|||
return errorType.isRetryable();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,5 +16,4 @@
|
|||
# under the License.
|
||||
|
||||
name=database-kms
|
||||
parent=kmsProvidersRegistry
|
||||
|
||||
parent=kms
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue