Add KMS framework

This commit is contained in:
vishesh92 2025-12-17 14:10:45 +05:30
parent b744824f65
commit e995b46b20
No known key found for this signature in database
GPG Key ID: 4E395186CBFA790B
44 changed files with 4971 additions and 1 deletions

View File

@ -71,6 +71,11 @@
<artifactId>cloud-framework-direct-download</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-framework-kms</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>

View File

@ -271,6 +271,14 @@ public class EventTypes {
public static final String EVENT_CA_CERTIFICATE_REVOKE = "CA.CERTIFICATE.REVOKE";
public static final String EVENT_CA_CERTIFICATE_PROVISION = "CA.CERTIFICATE.PROVISION";
// KMS (Key Management Service) events
public static final String EVENT_KMS_KEY_WRAP = "KMS.KEY.WRAP";
public static final String EVENT_KMS_KEY_UNWRAP = "KMS.KEY.UNWRAP";
public static final String EVENT_KMS_KEK_CREATE = "KMS.KEK.CREATE";
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";
// Account events
public static final String EVENT_ACCOUNT_ENABLE = "ACCOUNT.ENABLE";
public static final String EVENT_ACCOUNT_DISABLE = "ACCOUNT.DISABLE";

View File

@ -89,7 +89,8 @@ public enum ApiCommandResourceType {
KubernetesSupportedVersion(null),
SharedFS(org.apache.cloudstack.storage.sharedfs.SharedFS.class),
Extension(org.apache.cloudstack.extension.Extension.class),
ExtensionCustomAction(org.apache.cloudstack.extension.ExtensionCustomAction.class);
ExtensionCustomAction(org.apache.cloudstack.extension.ExtensionCustomAction.class),
KmsKey(org.apache.cloudstack.kms.KMSKey.class);
private final Class<?> clazz;

View File

@ -867,6 +867,9 @@ public class ApiConstants {
public static final String SORT_BY = "sortby";
public static final String CHANGE_CIDR = "changecidr";
public static final String PURPOSE = "purpose";
public static final String KMS_KEY_ID = "kmskeyid";
public static final String KEK_LABEL = "keklabel";
public static final String KEY_BITS = "keybits";
public static final String IS_TAGGED = "istagged";
public static final String INSTANCE_NAME = "instancename";
public static final String CONSIDER_LAST_HOST = "considerlasthost";

View File

@ -76,6 +76,8 @@ import org.apache.cloudstack.api.response.HypervisorGuestOsNamesResponse;
import org.apache.cloudstack.api.response.IPAddressResponse;
import org.apache.cloudstack.api.response.ImageStoreResponse;
import org.apache.cloudstack.api.response.InstanceGroupResponse;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.kms.KMSKey;
import org.apache.cloudstack.api.response.InternalLoadBalancerElementResponse;
import org.apache.cloudstack.api.response.IpForwardingRuleResponse;
import org.apache.cloudstack.api.response.IpQuarantineResponse;
@ -591,4 +593,6 @@ public interface ResponseGenerator {
ApiKeyPairResponse createKeyPairResponse(ApiKeyPair keyPair);
ListResponse<BaseRolePermissionResponse> createKeypairPermissionsResponse(List<ApiKeyPairPermission> permissions);
KMSKeyResponse createKMSKeyResponse(KMSKey kmsKey);
}

View File

@ -0,0 +1,166 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cloudstack.api.command.user.kms;
import com.cloud.exception.ResourceAllocationException;
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.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.user.UserCmd;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.kms.KMSManager;
import javax.inject.Inject;
@APICommand(name = "createKMSKey",
description = "Creates a new KMS key (Key Encryption Key) for encryption",
responseObject = KMSKeyResponse.class,
since = "4.23.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
public class CreateKMSKeyCmd extends BaseCmd implements UserCmd {
private static final String s_name = "createkmskeyresponse";
@Inject
private KMSManager kmsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.NAME,
required = true,
type = CommandType.STRING,
description = "Name of the KMS key")
private String name;
@Parameter(name = ApiConstants.DESCRIPTION,
type = CommandType.STRING,
description = "Description of the KMS key")
private String description;
@Parameter(name = ApiConstants.PURPOSE,
required = true,
type = CommandType.STRING,
description = "Purpose of the key: VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET")
private String purpose;
@Parameter(name = ApiConstants.ZONE_ID,
required = true,
type = CommandType.UUID,
entityType = ZoneResponse.class,
description = "Zone ID where the key will be valid")
private Long zoneId;
@Parameter(name = ApiConstants.ACCOUNT,
type = CommandType.STRING,
description = "Account name (for creating keys for child accounts - requires domain admin or admin)")
private String accountName;
@Parameter(name = ApiConstants.DOMAIN_ID,
type = CommandType.UUID,
entityType = DomainResponse.class,
description = "Domain ID (for creating keys for child accounts - requires domain admin or admin)")
private Long domainId;
@Parameter(name = ApiConstants.KEY_BITS,
type = CommandType.INTEGER,
description = "Key size in bits: 128, 192, or 256 (default: 256)")
private Integer keyBits;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public String getPurpose() {
return purpose;
}
public Long getZoneId() {
return zoneId;
}
public String getAccountName() {
return accountName;
}
public Long getDomainId() {
return domainId;
}
public Integer getKeyBits() {
return keyBits != null ? keyBits : 256; // Default to 256 bits
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() throws ResourceAllocationException {
try {
KMSKeyResponse response = kmsManager.createKMSKey(this);
response.setResponseName(getCommandName());
setResponseObject(response);
} catch (KMSException e) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR,
"Failed to create KMS key: " + e.getMessage());
}
}
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
Account caller = CallContext.current().getCallingAccount();
if (accountName != null || domainId != null) {
return _accountService.finalyzeAccountId(accountName, domainId, null, true);
}
return caller.getId();
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.KmsKey;
}
}

View File

@ -0,0 +1,113 @@
/*
* 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.user.kms;
import com.cloud.event.EventTypes;
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.command.user.UserCmd;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.kms.KMSManager;
import javax.inject.Inject;
@APICommand(name = "deleteKMSKey",
description = "Deletes a KMS key (only if not in use)",
responseObject = SuccessResponse.class,
since = "4.23.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
public class DeleteKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
private static final String s_name = "deletekmskeyresponse";
@Inject
private KMSManager kmsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID,
required = true,
type = CommandType.UUID,
entityType = KMSKeyResponse.class,
description = "The UUID of the KMS key to delete")
private Long id;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
try {
SuccessResponse response = kmsManager.deleteKMSKey(this);
response.setResponseName(getCommandName());
setResponseObject(response);
} catch (KMSException e) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR,
"Failed to delete KMS key: " + e.getMessage());
}
}
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccount().getId();
}
@Override
public String getEventType() {
return EventTypes.EVENT_KMS_KEK_DELETE;
}
@Override
public String getEventDescription() {
return "deleting KMS key: " + getId();
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.KmsKey;
}
}

View File

@ -0,0 +1,112 @@
/*
* 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.user.kms;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseListAccountResourcesCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.command.user.UserCmd;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.kms.KMSManager;
import javax.inject.Inject;
@APICommand(name = "listKMSKeys",
description = "Lists KMS keys available to the caller",
responseObject = KMSKeyResponse.class,
responseView = ResponseView.Restricted,
since = "4.23.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
public class ListKMSKeysCmd extends BaseListAccountResourcesCmd implements UserCmd {
private static final String s_name = "listkmskeysresponse";
@Inject
private KMSManager kmsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID,
type = CommandType.UUID,
entityType = KMSKeyResponse.class,
description = "List KMS key by UUID")
private Long id;
@Parameter(name = ApiConstants.PURPOSE,
type = CommandType.STRING,
description = "Filter by purpose: VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET")
private String purpose;
@Parameter(name = ApiConstants.ZONE_ID,
type = CommandType.UUID,
entityType = ZoneResponse.class,
description = "Filter by zone ID")
private Long zoneId;
@Parameter(name = ApiConstants.STATE,
type = CommandType.STRING,
description = "Filter by state: Enabled, Disabled")
private String state;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
public String getPurpose() {
return purpose;
}
public Long getZoneId() {
return zoneId;
}
public String getState() {
return state;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
ListResponse<KMSKeyResponse> listResponse = kmsManager.listKMSKeys(this);
listResponse.setResponseName(getCommandName());
setResponseObject(listResponse);
}
@Override
public String getCommandName() {
return s_name;
}
}

View File

@ -0,0 +1,138 @@
/*
* 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.user.kms;
import com.cloud.event.EventTypes;
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.command.user.UserCmd;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.kms.KMSManager;
import javax.inject.Inject;
@APICommand(name = "updateKMSKey",
description = "Updates KMS key name, description, or state",
responseObject = KMSKeyResponse.class,
since = "4.23.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
public class UpdateKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
private static final String s_name = "updatekmskeyresponse";
@Inject
private KMSManager kmsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID,
required = true,
type = CommandType.UUID,
entityType = KMSKeyResponse.class,
description = "The UUID of the KMS key to update")
private Long id;
@Parameter(name = ApiConstants.NAME,
type = CommandType.STRING,
description = "New name for the key")
private String name;
@Parameter(name = ApiConstants.DESCRIPTION,
type = CommandType.STRING,
description = "New description for the key")
private String description;
@Parameter(name = ApiConstants.STATE,
type = CommandType.STRING,
description = "New state: Enabled or Disabled")
private String state;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public String getState() {
return state;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
try {
KMSKeyResponse response = kmsManager.updateKMSKey(this);
response.setResponseName(getCommandName());
setResponseObject(response);
} catch (KMSException e) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR,
"Failed to update KMS key: " + e.getMessage());
}
}
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccount().getId();
}
@Override
public String getEventType() {
return EventTypes.EVENT_KMS_KEK_CREATE; // Reuse create event type for updates
}
@Override
public String getEventDescription() {
return "updating KMS key: " + getId();
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.KmsKey;
}
}

View File

@ -0,0 +1,256 @@
/*
* 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.response;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.api.EntityReference;
import org.apache.cloudstack.kms.KMSKey;
import java.util.Date;
@EntityReference(value = KMSKey.class)
public class KMSKeyResponse extends BaseResponse implements ControlledEntityResponse {
@SerializedName(ApiConstants.ID)
@Param(description = "the UUID of the key")
private String id;
@SerializedName(ApiConstants.NAME)
@Param(description = "the name of the key")
private String name;
@SerializedName(ApiConstants.DESCRIPTION)
@Param(description = "the description of the key")
private String description;
@SerializedName(ApiConstants.PURPOSE)
@Param(description = "the purpose of the key (VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET)")
private String purpose;
@SerializedName(ApiConstants.ACCOUNT)
@Param(description = "the account owning the key")
private String accountName;
@SerializedName(ApiConstants.ACCOUNT_ID)
@Param(description = "the account ID owning the key")
private String accountId;
@SerializedName(ApiConstants.DOMAIN_ID)
@Param(description = "the domain ID of the key")
private String domainId;
@SerializedName(ApiConstants.DOMAIN)
@Param(description = "the domain name of the key")
private String domainName;
@SerializedName(ApiConstants.DOMAIN_PATH)
@Param(description = "the domain path of the key")
private String domainPath;
@SerializedName(ApiConstants.ZONE_ID)
@Param(description = "the zone ID where the key is valid")
private String zoneId;
@SerializedName(ApiConstants.ZONE_NAME)
@Param(description = "the zone name where the key is valid")
private String zoneName;
@SerializedName(ApiConstants.PROVIDER)
@Param(description = "the KMS provider (database, pkcs11, etc.)")
private String provider;
@SerializedName(ApiConstants.ALGORITHM)
@Param(description = "the encryption algorithm")
private String algorithm;
@SerializedName(ApiConstants.KEY_BITS)
@Param(description = "the key size in bits")
private Integer keyBits;
@SerializedName(ApiConstants.STATE)
@Param(description = "the state of the key (Enabled, Disabled, Deleted)")
private String state;
@SerializedName(ApiConstants.CREATED)
@Param(description = "the creation timestamp")
private Date created;
// KEK label is admin-only for security
@SerializedName(ApiConstants.KEK_LABEL)
@Param(description = "the provider-specific KEK label (admin only)", authorized = {RoleType.Admin})
private String kekLabel;
// Getters and Setters
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getPurpose() {
return purpose;
}
public void setPurpose(String purpose) {
this.purpose = purpose;
}
public String getAccountName() {
return accountName;
}
@Override
public void setAccountName(String accountName) {
this.accountName = accountName;
}
@Override
public void setProjectId(String projectId) {
// KMS keys are not project-scoped
}
@Override
public void setProjectName(String projectName) {
// KMS keys are not project-scoped
}
public String getAccountId() {
return accountId;
}
public void setAccountId(String accountId) {
this.accountId = accountId;
}
public String getDomainId() {
return domainId;
}
@Override
public void setDomainId(String domainId) {
this.domainId = domainId;
}
public String getDomainName() {
return domainName;
}
@Override
public void setDomainName(String domainName) {
this.domainName = domainName;
}
public String getDomainPath() {
return domainPath;
}
@Override
public void setDomainPath(String domainPath) {
this.domainPath = domainPath;
}
public String getZoneId() {
return zoneId;
}
public void setZoneId(String zoneId) {
this.zoneId = zoneId;
}
public String getZoneName() {
return zoneName;
}
public void setZoneName(String zoneName) {
this.zoneName = zoneName;
}
public String getProvider() {
return provider;
}
public void setProvider(String provider) {
this.provider = provider;
}
public String getAlgorithm() {
return algorithm;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public Integer getKeyBits() {
return keyBits;
}
public void setKeyBits(Integer keyBits) {
this.keyBits = keyBits;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
public String getKekLabel() {
return kekLabel;
}
public void setKekLabel(String kekLabel) {
this.kekLabel = kekLabel;
}
}

View File

@ -0,0 +1,104 @@
/*
* 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;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
import org.apache.cloudstack.framework.kms.KeyPurpose;
import java.util.Date;
/**
* KMS Key (Key Encryption Key) metadata.
* Represents a KEK that can be used to wrap/unwrap Data Encryption Keys (DEKs).
* KEKs are account-scoped and used for envelope encryption.
*/
public interface KMSKey extends Identity, InternalIdentity, ControlledEntity {
/**
* Get the user-friendly name of the key
*/
String getName();
/**
* Get the description of the key
*/
String getDescription();
/**
* Get the provider-specific KEK label/ID
* (internal identifier used by the KMS provider)
*/
String getKekLabel();
/**
* Get the purpose of this key
*/
KeyPurpose getPurpose();
/**
* Get the zone ID where this key is valid
*/
Long getZoneId();
/**
* Get the KMS provider name (e.g., "database", "pkcs11")
*/
String getProviderName();
/**
* Get the encryption algorithm (e.g., "AES/GCM/NoPadding")
*/
String getAlgorithm();
/**
* Get the key size in bits (e.g., 128, 192, 256)
*/
Integer getKeyBits();
/**
* Get the current state of the key
*/
State getState();
/**
* Get the creation timestamp
*/
Date getCreated();
/**
* Get the removal timestamp (null if not removed)
*/
Date getRemoved();
/**
* Key state enumeration
*/
enum State {
/** Key is active and can be used for encryption/decryption */
Enabled,
/** Key is disabled and cannot be used for new operations */
Disabled,
/** Key is soft-deleted */
Deleted
}
}

View File

@ -0,0 +1,375 @@
// 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;
import com.cloud.utils.component.Manager;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
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;
import org.apache.cloudstack.api.command.user.kms.UpdateKMSKeyCmd;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
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 java.util.List;
/**
* Manager interface for Key Management Service operations.
* Provides high-level API for cryptographic key management with zone-scoping,
* provider abstraction, and integration with CloudStack's configuration system.
*/
public interface KMSManager extends Manager, Configurable {
// ==================== Configuration Keys ====================
/**
* Global: which KMS provider plugin to use by default
* Supported values: "database" (default), "pkcs11", or custom provider names
*/
ConfigKey<String> KMSProviderPlugin = new ConfigKey<>(
"Advanced",
String.class,
"kms.provider.plugin",
"database",
"The KMS provider plugin to use for cryptographic operations (database, pkcs11, etc.)",
true,
ConfigKey.Scope.Global
);
/**
* Zone-scoped: enable KMS for a specific zone
* When false (default), new volumes use legacy passphrase encryption
* When true, new volumes use KMS envelope encryption
*/
ConfigKey<Boolean> KMSEnabled = new ConfigKey<>(
"Advanced",
Boolean.class,
"kms.enabled",
"false",
"Enable Key Management Service for disk encryption in this zone",
true,
ConfigKey.Scope.Zone
);
/**
* Global: DEK size in bits for volume encryption
* Supported: 128, 192, 256
*/
ConfigKey<Integer> KMSDekSizeBits = new ConfigKey<>(
"Advanced",
Integer.class,
"kms.dek.size.bits",
"256",
"The size of Data Encryption Keys (DEK) in bits (128, 192, or 256)",
true,
ConfigKey.Scope.Global
);
/**
* Global: retry count for transient KMS failures
*/
ConfigKey<Integer> KMSRetryCount = new ConfigKey<>(
"Advanced",
Integer.class,
"kms.retry.count",
"3",
"Number of retry attempts for transient KMS failures",
true,
ConfigKey.Scope.Global
);
/**
* Global: retry delay in milliseconds
*/
ConfigKey<Integer> KMSRetryDelayMs = new ConfigKey<>(
"Advanced",
Integer.class,
"kms.retry.delay.ms",
"1000",
"Delay in milliseconds between KMS retry attempts (exponential backoff)",
true,
ConfigKey.Scope.Global
);
/**
* Global: timeout for KMS operations in seconds
*/
ConfigKey<Integer> KMSOperationTimeoutSec = new ConfigKey<>(
"Advanced",
Integer.class,
"kms.operation.timeout.sec",
"30",
"Timeout in seconds for KMS cryptographic operations",
true,
ConfigKey.Scope.Global
);
// ==================== Provider Management ====================
/**
* List all registered KMS providers
*
* @return list of available providers
*/
List<? extends KMSProvider> listKMSProviders();
/**
* Get a specific KMS provider by name
*
* @param name provider name
* @return the provider, or null if not found
*/
KMSProvider getKMSProvider(String name);
/**
* Get the configured provider for a zone
*
* @param zoneId the zone ID (null for global default)
* @return the configured provider
* @throws KMSException if no provider configured
*/
KMSProvider getKMSProviderForZone(Long zoneId) throws KMSException;
/**
* Check if KMS is enabled for a zone
*
* @param zoneId the zone ID
* @return true if KMS is enabled
*/
boolean isKmsEnabled(Long zoneId);
// ==================== KEK Management ====================
/**
* Create a new KEK for a zone and purpose
*
* @param zoneId the zone ID
* @param purpose the key purpose
* @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 (WARNING: makes all DEKs wrapped by it unrecoverable)
*
* @param zoneId the zone ID
* @param kekId the KEK identifier
* @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
* @param purpose the purpose filter (null for all)
* @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
*
* @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 (create new one and rewrap all DEKs)
*
* @param zoneId the zone ID
* @param purpose the purpose
* @param oldKekLabel the old KEK label (must be specified)
* @param newKekLabel the new KEK label (null for auto-generated)
* @param keyBits the new KEK size
* @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 ====================
/**
* Unwrap a DEK from a wrapped key
* SECURITY: Caller must zeroize returned byte array after use!
*
* @param wrappedKey the wrapped key from database
* @param zoneId the zone ID
* @return plaintext DEK (caller must zeroize!)
* @throws KMSException if unwrap fails
*/
byte[] unwrapVolumeKey(WrappedKey wrappedKey, Long zoneId) throws KMSException;
// ==================== Health & Status ====================
/**
* Check KMS provider health for a zone
*
* @param zoneId the zone ID (null for global)
* @return true if healthy
* @throws KMSException if health check fails critically
*/
boolean healthCheck(Long zoneId) throws KMSException;
// ==================== User KEK Management ====================
/**
* Create a new KMS key (KEK) for a user account
*
* @param accountId the account ID
* @param domainId the domain ID
* @param zoneId the zone ID
* @param name user-friendly name
* @param description optional description
* @param purpose key purpose
* @param keyBits key size in bits
* @return the created KMS key
* @throws KMSException if creation fails
*/
KMSKey createUserKMSKey(Long accountId, Long domainId, Long zoneId,
String name, String description, KeyPurpose purpose,
Integer keyBits) throws KMSException;
/**
* List KMS keys accessible to a user account
*
* @param accountId the account ID
* @param domainId the domain ID
* @param zoneId optional zone filter
* @param purpose optional purpose filter
* @param state optional state filter
* @return list of accessible KMS keys
*/
List<? extends KMSKey> listUserKMSKeys(Long accountId, Long domainId, Long zoneId,
KeyPurpose purpose, KMSKey.State state);
/**
* Get a KMS key by UUID (with permission check)
*
* @param uuid the key UUID
* @param callerAccountId the caller's account ID
* @return the KMS key, or null if not found or no permission
*/
KMSKey getUserKMSKey(String uuid, Long callerAccountId);
/**
* Check if caller has permission to use a KMS key
*
* @param callerAccountId the caller's account ID
* @param keyUuid the key UUID
* @return true if caller has permission
*/
boolean hasPermission(Long callerAccountId, String keyUuid);
/**
* 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
*
* @param wrappedKeyId the wrapped key database ID
* @return plaintext DEK (caller must zeroize!)
* @throws KMSException if unwrap fails
*/
byte[] unwrapKey(Long wrappedKeyId) throws KMSException;
/**
* Generate and wrap a DEK using a specific KMS key UUID
*
* @param kekUuid the KMS key UUID
* @param callerAccountId the caller's account ID
* @return wrapped key ready for database storage
* @throws KMSException if operation fails
*/
WrappedKey generateVolumeKeyWithKek(String kekUuid, Long callerAccountId) throws KMSException;
// ==================== API Response Methods ====================
/**
* Create a KMS key and return the response object.
* Handles validation, account resolution, and permission checks.
*
* @param cmd the create command with all parameters
* @return KMSKeyResponse
* @throws KMSException if creation fails
*/
KMSKeyResponse createKMSKey(CreateKMSKeyCmd cmd) throws KMSException;
/**
* List KMS keys and return the response object.
* Handles validation and permission checks.
*
* @param cmd the list command with all parameters
* @return ListResponse with KMSKeyResponse objects
*/
ListResponse<KMSKeyResponse> listKMSKeys(ListKMSKeysCmd cmd);
/**
* Update a KMS key and return the response object.
* Handles validation and permission checks.
*
* @param cmd the update command with all parameters
* @return KMSKeyResponse
* @throws KMSException if update fails
*/
KMSKeyResponse updateKMSKey(UpdateKMSKeyCmd cmd) throws KMSException;
/**
* Delete a KMS key and return the response object.
* Handles validation and permission checks.
*
* @param cmd the delete command with all parameters
* @return SuccessResponse
* @throws KMSException if deletion fails
*/
SuccessResponse deleteKMSKey(DeleteKMSKeyCmd cmd) throws KMSException;
}

View File

@ -366,4 +366,7 @@
<bean id="sharedFSProvidersRegistry" class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
</bean>
<bean id="kmsProvidersRegistry" class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
</bean>
</beans>

View File

@ -48,6 +48,11 @@
<artifactId>cloud-framework-db</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-framework-kms</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>

View File

@ -182,6 +182,9 @@ public class VolumeVO implements Volume {
@Column(name = "passphrase_id")
private Long passphraseId;
@Column(name = "kms_wrapped_key_id")
private Long kmsWrappedKeyId;
@Column(name = "encrypt_format")
private String encryptFormat;
@ -683,6 +686,10 @@ public class VolumeVO implements Volume {
public void setPassphraseId(Long id) { this.passphraseId = id; }
public Long getKmsWrappedKeyId() { return kmsWrappedKeyId; }
public void setKmsWrappedKeyId(Long id) { this.kmsWrappedKeyId = id; }
public String getEncryptFormat() { return encryptFormat; }
public void setEncryptFormat(String encryptFormat) { this.encryptFormat = encryptFormat; }

View File

@ -0,0 +1,189 @@
// 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;
import com.cloud.utils.db.GenericDao;
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 versions.
* Tracks multiple KEK versions per KMS key to support gradual rotation.
* During rotation, a new version is created (status=Active) and old versions
* are marked as Previous (still usable for decryption) or Archived (no longer used).
*/
@Entity
@Table(name = "kms_kek_versions")
public class KMSKekVersionVO {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "uuid", nullable = false, unique = true)
private String uuid;
@Column(name = "kms_key_id", nullable = false)
private Long kmsKeyId;
@Column(name = "version_number", nullable = false)
private Integer versionNumber;
@Column(name = "kek_label", nullable = false)
private String kekLabel;
@Column(name = "status", nullable = false, length = 32)
@Enumerated(EnumType.STRING)
private Status status;
@Column(name = GenericDao.CREATED_COLUMN, nullable = false)
@Temporal(TemporalType.TIMESTAMP)
private Date created;
@Column(name = GenericDao.REMOVED_COLUMN)
@Temporal(TemporalType.TIMESTAMP)
private Date removed;
/**
* Status of a KEK version
*/
public enum Status {
/**
* Active version - used for new encryption operations
*/
Active,
/**
* Previous version - still usable for decryption during rotation
*/
Previous,
/**
* Archived version - no longer used (after re-encryption complete)
*/
Archived
}
/**
* Default constructor (required by JPA)
*/
public KMSKekVersionVO() {
this.uuid = UUID.randomUUID().toString();
this.created = new Date();
this.status = Status.Active;
}
/**
* Constructor for creating a new KEK version
*
* @param kmsKeyId the KMS key ID this version belongs to
* @param versionNumber the version number (1, 2, 3, ...)
* @param kekLabel the provider-specific KEK label
* @param status the status (typically Active for new versions)
*/
public KMSKekVersionVO(Long kmsKeyId, Integer versionNumber, String kekLabel, Status status) {
this();
this.kmsKeyId = kmsKeyId;
this.versionNumber = versionNumber;
this.kekLabel = kekLabel;
this.status = status;
}
// Getters and Setters
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 Long getKmsKeyId() {
return kmsKeyId;
}
public void setKmsKeyId(Long kmsKeyId) {
this.kmsKeyId = kmsKeyId;
}
public Integer getVersionNumber() {
return versionNumber;
}
public void setVersionNumber(Integer versionNumber) {
this.versionNumber = versionNumber;
}
public String getKekLabel() {
return kekLabel;
}
public void setKekLabel(String kekLabel) {
this.kekLabel = kekLabel;
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
public Date getRemoved() {
return removed;
}
public void setRemoved(Date removed) {
this.removed = removed;
}
@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);
}
}

View File

@ -0,0 +1,277 @@
// 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;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.framework.kms.KeyPurpose;
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 KMS Key (Key Encryption Key) metadata.
* Tracks ownership, purpose, and lifecycle of KEKs used in envelope encryption.
*/
@Entity
@Table(name = "kms_keys")
public class KMSKeyVO implements KMSKey {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "uuid", nullable = false, unique = true)
private String uuid;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "description", length = 1024)
private String description;
@Column(name = "kek_label", nullable = false)
private String kekLabel;
@Column(name = "purpose", nullable = false, length = 32)
@Enumerated(EnumType.STRING)
private KeyPurpose purpose;
@Column(name = "account_id", nullable = false)
private Long accountId;
@Column(name = "domain_id", nullable = false)
private Long domainId;
@Column(name = "zone_id", nullable = false)
private Long zoneId;
@Column(name = "provider_name", nullable = false, length = 64)
private String providerName;
@Column(name = "algorithm", nullable = false, length = 64)
private String algorithm;
@Column(name = "key_bits", nullable = false)
private Integer keyBits;
@Column(name = "state", nullable = false, length = 32)
@Enumerated(EnumType.STRING)
private State state;
@Column(name = GenericDao.CREATED_COLUMN, nullable = false)
@Temporal(TemporalType.TIMESTAMP)
private Date created;
@Column(name = GenericDao.REMOVED_COLUMN)
@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) {
this();
this.name = name;
this.description = description;
this.kekLabel = kekLabel;
this.purpose = purpose;
this.accountId = accountId;
this.domainId = domainId;
this.zoneId = zoneId;
this.providerName = providerName;
this.algorithm = algorithm;
this.keyBits = keyBits;
}
// Identity interface methods
@Override
public long getId() {
return id;
}
@Override
public String getUuid() {
return uuid;
}
// KMSKey interface methods
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public String getKekLabel() {
return kekLabel;
}
@Override
public KeyPurpose getPurpose() {
return purpose;
}
@Override
public Long getZoneId() {
return zoneId;
}
@Override
public String getProviderName() {
return providerName;
}
@Override
public String getAlgorithm() {
return algorithm;
}
@Override
public Integer getKeyBits() {
return keyBits;
}
@Override
public State getState() {
return state;
}
@Override
public Date getCreated() {
return created;
}
@Override
public Date getRemoved() {
return removed;
}
// ControlledEntity interface methods
@Override
public long getAccountId() {
return accountId;
}
@Override
public long getDomainId() {
return domainId;
}
@Override
public Class<?> getEntityType() {
return KMSKey.class;
}
// Setters
public void setId(Long id) {
this.id = id;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public void setName(String name) {
this.name = name;
}
public void setDescription(String description) {
this.description = description;
}
public void setKekLabel(String kekLabel) {
this.kekLabel = kekLabel;
}
public void setPurpose(KeyPurpose purpose) {
this.purpose = purpose;
}
public void setAccountId(Long accountId) {
this.accountId = accountId;
}
public void setDomainId(Long domainId) {
this.domainId = domainId;
}
public void setZoneId(Long zoneId) {
this.zoneId = zoneId;
}
public void setProviderName(String providerName) {
this.providerName = providerName;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public void setKeyBits(Integer keyBits) {
this.keyBits = keyBits;
}
public void setState(State state) {
this.state = state;
}
public void setCreated(Date created) {
this.created = created;
}
public void setRemoved(Date removed) {
this.removed = removed;
}
@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);
}
}

View File

@ -0,0 +1,207 @@
// 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;
import com.cloud.utils.db.GenericDao;
import javax.persistence.Column;
import javax.persistence.Entity;
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.Arrays;
import java.util.Date;
import java.util.UUID;
/**
* Database entity for storing wrapped (encrypted) Data Encryption Keys.
* Each entry represents a DEK that has been encrypted by a Key Encryption Key (KEK).
* KEK metadata is stored in kms_keys table via the kms_key_id foreign key.
*/
@Entity
@Table(name = "kms_wrapped_key")
public class KMSWrappedKeyVO {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "uuid", nullable = false, unique = true)
private String uuid;
@Column(name = "kms_key_id")
private Long kmsKeyId;
@Column(name = "kek_version_id")
private Long kekVersionId;
@Column(name = "zone_id", nullable = false)
private Long zoneId;
@Column(name = "wrapped_blob", nullable = false, columnDefinition = "VARBINARY(4096)")
private byte[] wrappedBlob;
@Column(name = GenericDao.CREATED_COLUMN, nullable = false)
@Temporal(TemporalType.TIMESTAMP)
private Date created;
@Column(name = GenericDao.REMOVED_COLUMN)
@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();
this.kekVersionId = kekVersionId;
this.zoneId = kmsKey.getZoneId();
// Defensive copy
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;
}
public void setId(Long id) {
this.id = id;
}
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public Long getKmsKeyId() {
return kmsKeyId;
}
public void setKmsKeyId(Long kmsKeyId) {
this.kmsKeyId = kmsKeyId;
}
public Long getKekVersionId() {
return kekVersionId;
}
public void setKekVersionId(Long kekVersionId) {
this.kekVersionId = kekVersionId;
}
public Long getZoneId() {
return zoneId;
}
public void setZoneId(Long zoneId) {
this.zoneId = zoneId;
}
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;
}
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
public Date getRemoved() {
return removed;
}
public void setRemoved(Date removed) {
this.removed = removed;
}
@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 +
'}';
}
}

View File

@ -0,0 +1,60 @@
// 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.dao;
import com.cloud.utils.db.GenericDao;
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
*/
KMSKekVersionVO getActiveVersion(Long kmsKeyId);
/**
* Get all versions that can be used for decryption (Active and Previous)
*/
List<KMSKekVersionVO> getVersionsForDecryption(Long kmsKeyId);
/**
* List all versions for a KMS key
*/
List<KMSKekVersionVO> listByKmsKeyId(Long kmsKeyId);
/**
* Find a specific version by KMS key ID and version number
*/
KMSKekVersionVO findByKmsKeyIdAndVersion(Long kmsKeyId, Integer versionNumber);
/**
* Find a KEK version by KEK label
*/
KMSKekVersionVO findByKekLabel(String kekLabel);
}

View File

@ -0,0 +1,129 @@
// 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.dao;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import org.apache.cloudstack.kms.KMSKekVersionVO;
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;
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);
}
@Override
public KMSKekVersionVO getActiveVersion(Long kmsKeyId) {
SearchCriteria<KMSKekVersionVO> sc = activeVersionSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
sc.setParameters("status", KMSKekVersionVO.Status.Active);
return findOneBy(sc);
}
@Override
public List<KMSKekVersionVO> getVersionsForDecryption(Long kmsKeyId) {
SearchCriteria<KMSKekVersionVO> sc = decryptionVersionsSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
sc.setParameters("status", KMSKekVersionVO.Status.Active, KMSKekVersionVO.Status.Previous);
return listBy(sc);
}
@Override
public List<KMSKekVersionVO> listByKmsKeyId(Long kmsKeyId) {
SearchCriteria<KMSKekVersionVO> sc = kmsKeyIdSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
return listBy(sc);
}
@Override
public KMSKekVersionVO findByKmsKeyIdAndVersion(Long kmsKeyId, Integer versionNumber) {
SearchCriteria<KMSKekVersionVO> sc = versionNumberSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
sc.setParameters("versionNumber", versionNumber);
return findOneBy(sc);
}
@Override
public KMSKekVersionVO findByKekLabel(String kekLabel) {
SearchCriteria<KMSKekVersionVO> sc = kekLabelSearch.create();
sc.setParameters("kekLabel", kekLabel);
return findOneBy(sc);
}
}

View File

@ -0,0 +1,72 @@
// 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.dao;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.framework.kms.KeyPurpose;
import org.apache.cloudstack.kms.KMSKey;
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
*/
KMSKeyVO findByKekLabel(String kekLabel, String providerName);
/**
* List KMS keys owned by an account
*/
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
*/
List<KMSKeyVO> listByZone(Long zoneId, KeyPurpose purpose, KMSKey.State state);
/**
* List KMS keys accessible to an account (owns or in parent domain)
*/
List<KMSKeyVO> listAccessibleKeys(Long accountId, Long domainId, Long zoneId, KeyPurpose purpose, KMSKey.State state);
/**
* Count how many wrapped keys reference this KEK
*/
long countWrappedKeysByKmsKey(Long kmsKeyId);
/**
* Count KEKs by label (to check for duplicates)
*/
long countByKekLabel(String kekLabel, String providerName);
}

View File

@ -0,0 +1,189 @@
// 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.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.KMSKey;
import org.apache.cloudstack.kms.KMSKeyVO;
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;
@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);
}
@Override
public KMSKeyVO findByKekLabel(String kekLabel, String providerName) {
SearchCriteria<KMSKeyVO> sc = kekLabelSearch.create();
sc.setParameters("kekLabel", kekLabel);
sc.setParameters("providerName", providerName);
return findOneBy(sc);
}
@Override
public List<KMSKeyVO> listByAccount(Long accountId, KeyPurpose purpose, KMSKey.State state) {
SearchCriteria<KMSKeyVO> sc = accountSearch.create();
sc.setParameters("accountId", accountId);
if (purpose != null) {
sc.setParameters("purpose", purpose);
}
if (state != null) {
sc.setParameters("state", state);
}
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();
sc.setParameters("zoneId", zoneId);
if (purpose != null) {
sc.setParameters("purpose", purpose);
}
if (state != null) {
sc.setParameters("state", state);
}
return listBy(sc);
}
@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
sc.setParameters("accountId", accountId);
if (zoneId != null) {
sc.setParameters("zoneId", zoneId);
}
if (purpose != null) {
sc.setParameters("purpose", purpose);
}
if (state != null) {
sc.setParameters("state", state);
}
return listBy(sc);
}
@Override
public long countWrappedKeysByKmsKey(Long kmsKeyId) {
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();
sc.setParameters("kekLabel", kekLabel);
sc.setParameters("providerName", providerName);
Integer count = getCount(sc);
return count != null ? count.longValue() : 0L;
}
}

View File

@ -0,0 +1,73 @@
// 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.dao;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.kms.KMSWrappedKeyVO;
import java.util.List;
/**
* Data Access Object for KMS Wrapped Keys.
* This DAO is purpose-agnostic and can be used for any key purpose
* (volumes, TLS certs, config secrets, etc.)
*/
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)
*
* @param kmsKeyId the KMS key ID (FK to kms_keys)
* @return list of wrapped keys
*/
List<KMSWrappedKeyVO> listByKmsKeyId(Long kmsKeyId);
/**
* List all wrapped keys in a zone
*
* @param zoneId the zone ID
* @return list of wrapped keys
*/
List<KMSWrappedKeyVO> listByZone(Long zoneId);
/**
* Count wrapped keys using a specific KMS key
*
* @param kmsKeyId the KMS key ID (FK to kms_keys)
* @return count of keys
*/
long countByKmsKeyId(Long kmsKeyId);
/**
* List all wrapped keys using a specific KEK version
*
* @param kekVersionId the KEK version ID (FK to kms_kek_versions)
* @return list of wrapped keys
*/
List<KMSWrappedKeyVO> listByKekVersionId(Long kekVersionId);
}

View File

@ -0,0 +1,103 @@
// 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.dao;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import org.apache.cloudstack.kms.KMSWrappedKeyVO;
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;
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();
// 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);
}
@Override
public List<KMSWrappedKeyVO> listByKmsKeyId(Long kmsKeyId) {
SearchCriteria<KMSWrappedKeyVO> sc = kmsKeyIdSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
return listBy(sc);
}
@Override
public List<KMSWrappedKeyVO> listByZone(Long zoneId) {
SearchCriteria<KMSWrappedKeyVO> sc = zoneSearch.create();
sc.setParameters("zoneId", zoneId);
return listBy(sc);
}
@Override
public long countByKmsKeyId(Long kmsKeyId) {
SearchCriteria<KMSWrappedKeyVO> sc = kmsKeyIdSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
Integer count = getCount(sc);
return count != null ? count.longValue() : 0L;
}
@Override
public List<KMSWrappedKeyVO> listByKekVersionId(Long kekVersionId) {
SearchCriteria<KMSWrappedKeyVO> sc = kekVersionIdSearch.create();
sc.setParameters("kekVersionId", kekVersionId);
return listBy(sc);
}
}

View File

@ -312,4 +312,7 @@
<bean id="importVMTaskDaoImpl" class="com.cloud.vm.dao.ImportVMTaskDaoImpl" />
<bean id="apiKeyPairDaoImpl" class="org.apache.cloudstack.acl.dao.ApiKeyPairDaoImpl" />
<bean id="apiKeyPairPermissionsDaoImpl" class="org.apache.cloudstack.acl.dao.ApiKeyPairPermissionsDaoImpl" />
<bean id="kmsKeyDaoImpl" class="org.apache.cloudstack.kms.dao.KMSKeyDaoImpl" />
<bean id="kmsKekVersionDaoImpl" class="org.apache.cloudstack.kms.dao.KMSKekVersionDaoImpl" />
<bean id="kmsWrappedKeyDaoImpl" class="org.apache.cloudstack.kms.dao.KMSWrappedKeyDaoImpl" />
</beans>

View File

@ -63,6 +63,7 @@ CREATE TABLE IF NOT EXISTS `cloud`.`webhook_filter` (
CONSTRAINT `fk_webhook_filter__webhook_id` FOREIGN KEY(`webhook_id`) REFERENCES `webhook`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- "api_keypair" table for API and secret keys
CREATE TABLE IF NOT EXISTS `cloud`.`api_keypair` (
`id` bigint(20) unsigned NOT NULL auto_increment,
@ -114,3 +115,77 @@ CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Resource Admin', 'deleteUserKey
-- Add conserve mode for VPC offerings
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','conserve_mode', 'tinyint(1) unsigned NULL DEFAULT 0 COMMENT ''True if the VPC offering is IP conserve mode enabled, allowing public IP services to be used across multiple VPC tiers'' ');
-- KMS Keys (Key Encryption Key Metadata)
-- Account-scoped KEKs for envelope encryption
CREATE TABLE IF NOT EXISTS `cloud`.`kms_keys` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique ID',
`uuid` VARCHAR(40) NOT NULL COMMENT 'UUID - user-facing identifier',
`name` VARCHAR(255) NOT NULL COMMENT 'User-friendly name',
`description` VARCHAR(1024) COMMENT 'User description',
`kek_label` VARCHAR(255) NOT NULL COMMENT 'Provider-specific KEK label/ID',
`purpose` VARCHAR(32) NOT NULL COMMENT 'Key purpose (VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET)',
`account_id` BIGINT UNSIGNED NOT NULL COMMENT 'Owning account',
`domain_id` BIGINT UNSIGNED NOT NULL COMMENT 'Owning domain',
`zone_id` BIGINT UNSIGNED NOT NULL COMMENT 'Zone where key is valid',
`provider_name` VARCHAR(64) NOT NULL COMMENT 'KMS provider (database, pkcs11, etc.)',
`algorithm` VARCHAR(64) NOT NULL DEFAULT 'AES/GCM/NoPadding' COMMENT 'Encryption algorithm',
`key_bits` INT NOT NULL DEFAULT 256 COMMENT 'Key size in bits',
`state` VARCHAR(32) NOT NULL DEFAULT 'Enabled' COMMENT 'Enabled, Disabled, or Deleted',
`created` DATETIME NOT NULL COMMENT 'Creation timestamp',
`removed` DATETIME COMMENT 'Removal timestamp for soft delete',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_uuid` (`uuid`),
INDEX `idx_account_purpose` (`account_id`, `purpose`, `state`),
INDEX `idx_domain_purpose` (`domain_id`, `purpose`, `state`),
INDEX `idx_zone_state` (`zone_id`, `state`),
INDEX `idx_kek_label_provider` (`kek_label`, `provider_name`),
CONSTRAINT `fk_kms_keys__account_id` FOREIGN KEY (`account_id`) REFERENCES `account`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_kms_keys__domain_id` FOREIGN KEY (`domain_id`) REFERENCES `domain`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_kms_keys__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KMS Key (KEK) metadata - account-scoped keys for envelope encryption';
-- KMS KEK Versions (multiple KEKs per KMS key for gradual rotation)
-- Supports multiple KEK versions per logical KMS key during rotation
CREATE TABLE IF NOT EXISTS `cloud`.`kms_kek_versions` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique ID',
`uuid` VARCHAR(40) NOT NULL COMMENT 'UUID',
`kms_key_id` BIGINT UNSIGNED NOT NULL COMMENT 'Reference to kms_keys table',
`version_number` INT NOT NULL COMMENT 'Version number (1, 2, 3, ...)',
`kek_label` VARCHAR(255) NOT NULL COMMENT 'Provider-specific KEK label/ID for this version',
`status` VARCHAR(32) NOT NULL DEFAULT 'Active' COMMENT 'Active, Previous, Archived',
`created` DATETIME NOT NULL COMMENT 'Creation timestamp',
`removed` DATETIME COMMENT 'Removal timestamp for soft delete',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_uuid` (`uuid`),
UNIQUE KEY `uk_kms_key_version` (`kms_key_id`, `version_number`, `removed`),
INDEX `idx_kms_key_status` (`kms_key_id`, `status`, `removed`),
INDEX `idx_kek_label` (`kek_label`),
CONSTRAINT `fk_kms_kek_versions__kms_key_id` FOREIGN KEY (`kms_key_id`) REFERENCES `kms_keys`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KEK versions for a KMS key - supports gradual rotation';
-- KMS Wrapped Keys (Data Encryption Keys)
-- Generic table for wrapped DEKs - references kms_keys for metadata and kek_versions for specific KEK version
CREATE TABLE IF NOT EXISTS `cloud`.`kms_wrapped_key` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique ID',
`uuid` VARCHAR(40) NOT NULL COMMENT 'UUID',
`kms_key_id` BIGINT UNSIGNED COMMENT 'Reference to kms_keys table',
`kek_version_id` BIGINT UNSIGNED COMMENT 'Reference to kms_kek_versions table',
`zone_id` BIGINT UNSIGNED NOT NULL COMMENT 'Zone ID for zone-scoped keys',
`wrapped_blob` VARBINARY(4096) NOT NULL COMMENT 'Encrypted DEK material',
`created` DATETIME NOT NULL COMMENT 'Creation timestamp',
`removed` DATETIME COMMENT 'Removal timestamp for soft delete',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_uuid` (`uuid`),
INDEX `idx_kms_key_id` (`kms_key_id`, `removed`),
INDEX `idx_kek_version_id` (`kek_version_id`, `removed`),
INDEX `idx_zone_id` (`zone_id`, `removed`),
CONSTRAINT `fk_kms_wrapped_key__kms_key_id` FOREIGN KEY (`kms_key_id`) REFERENCES `kms_keys`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_kms_wrapped_key__kek_version_id` FOREIGN KEY (`kek_version_id`) REFERENCES `kms_kek_versions`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_kms_wrapped_key__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KMS wrapped encryption keys (DEKs) - references kms_keys for KEK metadata and kek_versions for specific version';
-- Add KMS wrapped key reference to volumes table
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.volumes', 'kms_wrapped_key_id', 'BIGINT UNSIGNED COMMENT ''KMS wrapped key ID for volume encryption''');
CALL `cloud`.`IDEMPOTENT_ADD_FOREIGN_KEY`('cloud.volumes', 'fk_volumes__kms_wrapped_key_id', '(kms_wrapped_key_id)', '`kms_wrapped_key`(`id`)');

29
framework/kms/pom.xml Normal file
View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<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-framework-kms</artifactId>
<name>Apache CloudStack Framework - Key Management Service</name>
<description>Core KMS framework with provider-agnostic interfaces</description>
<parent>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloudstack-framework</artifactId>
<version>4.23.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-utils</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-framework-config</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,179 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.framework.kms;
import com.cloud.utils.exception.CloudRuntimeException;
/**
* Exception class for KMS-related errors with structured error types
* to enable proper retry logic and error handling.
*/
public class KMSException extends CloudRuntimeException {
/**
* Error types for KMS operations to enable intelligent retry logic
*/
public enum ErrorType {
/**
* Provider not initialized or unavailable
*/
PROVIDER_NOT_INITIALIZED(false),
/**
* KEK not found in backend
*/
KEK_NOT_FOUND(false),
/**
* KEK with given label already exists
*/
KEY_ALREADY_EXISTS(false),
/**
* Invalid parameters provided
*/
INVALID_PARAMETER(false),
/**
* Wrap/unwrap operation failed
*/
WRAP_UNWRAP_FAILED(true),
/**
* KEK operation (create/delete) failed
*/
KEK_OPERATION_FAILED(true),
/**
* Health check failed
*/
HEALTH_CHECK_FAILED(true),
/**
* Transient network or communication error
*/
TRANSIENT_ERROR(true),
/**
* Unknown error
*/
UNKNOWN(false);
private final boolean retryable;
ErrorType(boolean retryable) {
this.retryable = retryable;
}
public boolean isRetryable() {
return retryable;
}
}
private final ErrorType errorType;
public KMSException(String message) {
super(message);
this.errorType = ErrorType.UNKNOWN;
}
public KMSException(String message, Throwable cause) {
super(message, cause);
this.errorType = ErrorType.UNKNOWN;
}
public KMSException(ErrorType errorType, String message) {
super(message);
this.errorType = errorType;
}
public KMSException(ErrorType errorType, String message, Throwable cause) {
super(message, cause);
this.errorType = errorType;
}
public static KMSException providerNotInitialized(String details) {
return new KMSException(ErrorType.PROVIDER_NOT_INITIALIZED,
"KMS provider not initialized: " + details);
}
public static KMSException kekNotFound(String kekId) {
return new KMSException(ErrorType.KEK_NOT_FOUND,
"KEK not found: " + kekId);
}
// Static factory methods for common error types
public static KMSException keyAlreadyExists(String details) {
return new KMSException(ErrorType.KEY_ALREADY_EXISTS,
"Key already exists: " + details);
}
public static KMSException invalidParameter(String details) {
return new KMSException(ErrorType.INVALID_PARAMETER,
"Invalid parameter: " + details);
}
public static KMSException wrapUnwrapFailed(String details, Throwable cause) {
return new KMSException(ErrorType.WRAP_UNWRAP_FAILED,
"Wrap/unwrap operation failed: " + details, cause);
}
public static KMSException wrapUnwrapFailed(String details) {
return new KMSException(ErrorType.WRAP_UNWRAP_FAILED,
"Wrap/unwrap operation failed: " + details);
}
public static KMSException kekOperationFailed(String details, Throwable cause) {
return new KMSException(ErrorType.KEK_OPERATION_FAILED,
"KEK operation failed: " + details, cause);
}
public static KMSException kekOperationFailed(String details) {
return new KMSException(ErrorType.KEK_OPERATION_FAILED,
"KEK operation failed: " + details);
}
public static KMSException healthCheckFailed(String details, Throwable cause) {
return new KMSException(ErrorType.HEALTH_CHECK_FAILED,
"Health check failed: " + details, cause);
}
public static KMSException transientError(String details, Throwable cause) {
return new KMSException(ErrorType.TRANSIENT_ERROR,
"Transient error: " + details, cause);
}
public ErrorType getErrorType() {
return errorType;
}
@Override
public String toString() {
return "KMSException{" +
"errorType=" + errorType +
", retryable=" + isRetryable() +
", message='" + getMessage() + '\'' +
'}';
}
public boolean isRetryable() {
return errorType.isRetryable();
}
}

View File

@ -0,0 +1,144 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.framework.kms;
import org.apache.cloudstack.framework.config.Configurable;
import java.util.List;
/**
* Abstract provider contract for Key Management Service operations.
* <p>
* Implementations provide the cryptographic backend (HSM via PKCS#11, database, cloud KMS, etc.)
* for secure key wrapping/unwrapping using envelope encryption.
* <p>
* Design principles:
* - KEKs (Key Encryption Keys) never leave the secure backend
* - DEKs (Data Encryption Keys) are wrapped by KEKs for storage
* - Plaintext DEKs exist only transiently in memory during wrap/unwrap
* - All operations are purpose-scoped to prevent key reuse
* <p>
* Thread-safety: Implementations must be thread-safe for concurrent operations.
*/
public interface KMSProvider extends Configurable {
/**
* Get the unique name of this provider
*
* @return provider name (e.g., "database", "pkcs11")
*/
String getProviderName();
// ==================== KEK Management ====================
/**
* Create a new Key Encryption Key (KEK) in the secure backend
*
* @param purpose the purpose/scope for this KEK
* @param label human-readable label for the KEK (must be unique within purpose)
* @param keyBits key size in bits (typically 128, 192, or 256)
* @return the KEK identifier (label or handle) for later reference
* @throws KMSException if KEK creation fails
*/
String createKek(KeyPurpose purpose, String label, int keyBits) throws KMSException;
/**
* Delete a KEK from the secure backend.
* WARNING: This will make all DEKs wrapped by this KEK unrecoverable.
*
* @param kekId the KEK identifier to delete
* @throws KMSException if deletion fails or KEK not found
*/
void deleteKek(String kekId) throws KMSException;
/**
* List all KEK identifiers for a given purpose
*
* @param purpose the key purpose to filter by (null = all purposes)
* @return list of KEK identifiers
* @throws KMSException if listing fails
*/
List<String> listKeks(KeyPurpose purpose) throws KMSException;
/**
* Check if a KEK exists and is accessible
*
* @param kekId the KEK identifier to check
* @return true if KEK is available
* @throws KMSException if check fails
*/
boolean isKekAvailable(String kekId) throws KMSException;
// ==================== DEK Operations ====================
/**
* Wrap (encrypt) a plaintext Data Encryption Key with a KEK
*
* @param plainDek the plaintext DEK to wrap (caller must zeroize after call)
* @param purpose the intended purpose of this DEK
* @param kekLabel the label of the KEK to use for wrapping
* @return WrappedKey containing the encrypted DEK and metadata
* @throws KMSException if wrapping fails or KEK not found
*/
WrappedKey wrapKey(byte[] plainDek, KeyPurpose purpose, String kekLabel) throws KMSException;
/**
* Unwrap (decrypt) a wrapped DEK to obtain the plaintext key
* <p>
* SECURITY: Caller MUST zeroize the returned byte array after use
*
* @param wrappedKey the wrapped key to decrypt
* @return plaintext DEK (caller must zeroize!)
* @throws KMSException if unwrapping fails or KEK not found
*/
byte[] unwrapKey(WrappedKey wrappedKey) throws KMSException;
/**
* Generate a new random DEK and immediately wrap it with a KEK
* (convenience method combining generation + wrapping)
*
* @param purpose the intended purpose of the new DEK
* @param kekLabel the label of the KEK to use for wrapping
* @param keyBits DEK size in bits (typically 128, 192, or 256)
* @return WrappedKey containing the newly generated and wrapped DEK
* @throws KMSException if generation or wrapping fails
*/
WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits) throws KMSException;
/**
* Rewrap a DEK with a different KEK (used during key rotation).
* This unwraps with the old KEK and wraps with the new KEK without exposing the plaintext DEK.
*
* @param oldWrappedKey the currently wrapped key
* @param newKekLabel the label of the new KEK to wrap with
* @return new WrappedKey encrypted with the new KEK
* @throws KMSException if rewrapping fails
*/
WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel) throws KMSException;
// ==================== Health & Status ====================
/**
* Perform health check on the provider backend
*
* @return true if provider is healthy and operational
* @throws KMSException if health check fails with critical error
*/
boolean healthCheck() throws KMSException;
}

View File

@ -0,0 +1,166 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.framework.kms;
import java.util.List;
/**
* High-level service interface for Key Management Service operations.
* <p>
* This facade abstracts provider-specific details and provides zone-aware
* routing, retry logic, and audit logging for KMS operations.
* <p>
* The service handles:
* - Zone-scoped provider selection
* - Configuration management (which provider, which KEK)
* - Retry logic for transient failures
* - Audit event emission
* - Health monitoring
*/
public interface KMSService {
/**
* Get the service name
*
* @return service name
*/
String getName();
// ==================== Provider Management ====================
/**
* List all registered KMS providers
*
* @return list of available providers
*/
List<? extends KMSProvider> listProviders();
/**
* Get a specific provider by name
*
* @param name provider name
* @return the provider, or null if not found
*/
KMSProvider getProvider(String name);
/**
* Get the configured provider for a specific zone.
* Falls back to global default if zone has no specific configuration.
*
* @param zoneId the zone ID (null for global)
* @return the configured provider for the zone
* @throws KMSException if no provider configured or provider not found
*/
KMSProvider getProviderForZone(Long zoneId) throws KMSException;
// ==================== KEK Management ====================
/**
* Create a new KEK for a specific zone and purpose
*
* @param zoneId the zone ID (null for global)
* @param purpose the purpose of the KEK
* @param label optional custom label (null for auto-generated)
* @param keyBits key size in bits
* @return the KEK identifier
* @throws KMSException if creation fails
*/
String createKek(Long zoneId, KeyPurpose purpose, String label, int keyBits) throws KMSException;
/**
* Delete a KEK (use with extreme caution!)
*
* @param zoneId the zone ID
* @param kekId the KEK identifier to delete
* @throws KMSException if deletion fails
*/
void deleteKek(Long zoneId, String kekId) throws KMSException;
/**
* List KEKs for a zone and purpose
*
* @param zoneId the zone ID (null for all zones)
* @param purpose the purpose filter (null for all purposes)
* @return list of KEK identifiers
* @throws KMSException if listing fails
*/
List<String> listKeks(Long zoneId, KeyPurpose purpose) throws KMSException;
/**
* Check if a KEK is available in a zone
*
* @param zoneId the zone ID
* @param kekId the KEK identifier
* @return true if available
* @throws KMSException if check fails
*/
boolean isKekAvailable(Long zoneId, String kekId) throws KMSException;
/**
* Rotate a KEK by creating a new one and rewrapping all associated DEKs.
* This is an async operation that may take time for large deployments.
*
* @param zoneId the zone ID
* @param purpose the purpose of keys to rotate
* @param oldKekLabel the current KEK label (null for configured default)
* @param newKekLabel the new KEK label (null for auto-generated)
* @param keyBits the new KEK size in bits
* @return the new KEK identifier
* @throws KMSException if rotation fails
*/
String rotateKek(Long zoneId, KeyPurpose purpose, String oldKekLabel,
String newKekLabel, int keyBits) throws KMSException;
// ==================== DEK Operations ====================
/**
* Generate and wrap a new DEK for volume encryption
*
* @param zoneId the zone ID where the volume resides
* @param purpose the key purpose (typically VOLUME_ENCRYPTION)
* @param kekLabel the KEK label to use (null for configured default)
* @param keyBits DEK size in bits
* @return wrapped key ready for database storage
* @throws KMSException if operation fails
*/
WrappedKey generateAndWrapDek(Long zoneId, KeyPurpose purpose,
String kekLabel, int keyBits) throws KMSException;
/**
* Unwrap a DEK for use (e.g., attaching encrypted volume)
* <p>
* SECURITY: Caller must zeroize the returned byte array after use
*
* @param wrappedKey the wrapped key from database
* @return plaintext DEK (caller must zeroize!)
* @throws KMSException if unwrap fails
*/
byte[] unwrapDek(WrappedKey wrappedKey) throws KMSException;
// ==================== Health & Status ====================
/**
* Check health of KMS provider for a zone
*
* @param zoneId the zone ID (null for global check)
* @return true if healthy
* @throws KMSException if health check fails critically
*/
boolean healthCheck(Long zoneId) throws KMSException;
}

View File

@ -0,0 +1,82 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.framework.kms;
/**
* Defines the purpose/usage scope for cryptographic keys in the KMS system.
* This enables proper key segregation and prevents key reuse across different contexts.
*/
public enum KeyPurpose {
/**
* Keys used for encrypting VM disk volumes (LUKS, encrypted storage)
*/
VOLUME_ENCRYPTION("volume", "Volume disk encryption keys"),
/**
* Keys used for protecting TLS certificate private keys
*/
TLS_CERT("tls", "TLS certificate private keys"),
/**
* Keys used for encrypting configuration secrets and sensitive settings
*/
CONFIG_SECRET("config", "Configuration secrets");
private final String name;
private final String description;
KeyPurpose(String name, String description) {
this.name = name;
this.description = description;
}
/**
* Convert string name to KeyPurpose enum
*
* @param name the string representation of the purpose
* @return matching KeyPurpose
* @throws IllegalArgumentException if no matching purpose found
*/
public static KeyPurpose fromString(String name) {
for (KeyPurpose purpose : KeyPurpose.values()) {
if (purpose.getName().equalsIgnoreCase(name)) {
return purpose;
}
}
throw new IllegalArgumentException("Unknown KeyPurpose: " + name);
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
/**
* Generate a KEK label with purpose prefix
*
* @param customLabel optional custom label suffix
* @return formatted KEK label (e.g., "volume-kek-v1")
*/
public String generateKekLabel(String customLabel) {
return name + "-kek-" + (customLabel != null ? customLabel : "v1");
}
}

View File

@ -0,0 +1,165 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.framework.kms;
import java.util.Arrays;
import java.util.Date;
import java.util.Objects;
/**
* Immutable Data Transfer Object representing an encrypted (wrapped) Data Encryption Key.
* The wrapped key material contains the DEK encrypted by a Key Encryption Key (KEK)
* stored in a secure backend (HSM, database, etc.).
* <p>
* This follows the envelope encryption pattern:
* - DEK: encrypts actual data (e.g., disk volume)
* - KEK: encrypts the DEK (never leaves secure storage)
* - Wrapped Key: DEK encrypted by KEK, safe to store in database
*/
public class WrappedKey {
private final String id;
private final String kekId;
private final KeyPurpose purpose;
private final String algorithm;
private final byte[] wrappedKeyMaterial;
private final String providerName;
private final Date created;
private final Long zoneId;
/**
* Create a new WrappedKey instance
*
* @param kekId ID/label of the KEK used to wrap this key
* @param purpose the intended use of this key
* @param algorithm encryption algorithm (e.g., "AES/GCM/NoPadding")
* @param wrappedKeyMaterial the encrypted DEK blob
* @param providerName name of the KMS provider that created this key
* @param created timestamp when key was wrapped
* @param zoneId optional zone ID for zone-scoped keys
*/
public WrappedKey(String kekId, KeyPurpose purpose, String algorithm,
byte[] wrappedKeyMaterial, String providerName,
Date created, Long zoneId) {
this.id = null; // Will be set when persisted to DB
this.kekId = Objects.requireNonNull(kekId, "kekId cannot be null");
this.purpose = Objects.requireNonNull(purpose, "purpose cannot be null");
this.algorithm = Objects.requireNonNull(algorithm, "algorithm cannot be null");
this.providerName = providerName;
// Defensive copy to prevent external modification
if (wrappedKeyMaterial == null || wrappedKeyMaterial.length == 0) {
throw new IllegalArgumentException("wrappedKeyMaterial cannot be null or empty");
}
this.wrappedKeyMaterial = Arrays.copyOf(wrappedKeyMaterial, wrappedKeyMaterial.length);
this.created = created != null ? new Date(created.getTime()) : new Date();
this.zoneId = zoneId;
}
/**
* Constructor for database-loaded keys with ID
*/
public WrappedKey(String id, String kekId, KeyPurpose purpose, String algorithm,
byte[] wrappedKeyMaterial, String providerName,
Date created, Long zoneId) {
this.id = id;
this.kekId = Objects.requireNonNull(kekId, "kekId cannot be null");
this.purpose = Objects.requireNonNull(purpose, "purpose cannot be null");
this.algorithm = Objects.requireNonNull(algorithm, "algorithm cannot be null");
this.providerName = providerName;
if (wrappedKeyMaterial == null || wrappedKeyMaterial.length == 0) {
throw new IllegalArgumentException("wrappedKeyMaterial cannot be null or empty");
}
this.wrappedKeyMaterial = Arrays.copyOf(wrappedKeyMaterial, wrappedKeyMaterial.length);
this.created = created != null ? new Date(created.getTime()) : new Date();
this.zoneId = zoneId;
}
public String getId() {
return id;
}
public String getKekId() {
return kekId;
}
public KeyPurpose getPurpose() {
return purpose;
}
public String getAlgorithm() {
return algorithm;
}
/**
* Get wrapped key material. Returns a defensive copy to prevent modification.
* Caller is responsible for zeroizing the returned array after use.
*/
public byte[] getWrappedKeyMaterial() {
return Arrays.copyOf(wrappedKeyMaterial, wrappedKeyMaterial.length);
}
public String getProviderName() {
return providerName;
}
public Date getCreated() {
return created != null ? new Date(created.getTime()) : null;
}
public Long getZoneId() {
return zoneId;
}
@Override
public int hashCode() {
int result = Objects.hash(id, kekId, purpose, algorithm, providerName);
result = 31 * result + Arrays.hashCode(wrappedKeyMaterial);
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WrappedKey that = (WrappedKey) o;
return Objects.equals(id, that.id) &&
Objects.equals(kekId, that.kekId) &&
purpose == that.purpose &&
Objects.equals(algorithm, that.algorithm) &&
Arrays.equals(wrappedKeyMaterial, that.wrappedKeyMaterial) &&
Objects.equals(providerName, that.providerName);
}
@Override
public String toString() {
return "WrappedKey{" +
"id='" + id + '\'' +
", kekId='" + kekId + '\'' +
", purpose=" + purpose +
", algorithm='" + algorithm + '\'' +
", providerName='" + providerName + '\'' +
", materialLength=" + (wrappedKeyMaterial != null ? wrappedKeyMaterial.length : 0) +
", created=" + created +
", zoneId=" + zoneId +
'}';
}
}

View File

@ -54,6 +54,7 @@
<module>extensions</module>
<module>ipc</module>
<module>jobs</module>
<module>kms</module>
<module>managed-context</module>
<module>quota</module>
<module>rest</module>

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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>
<name>Apache CloudStack Plugin - KMS Database Provider</name>
<description>Database-backed KMS provider for encrypted key storage</description>
<parent>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloudstack-kms-plugins</artifactId>
<version>4.23.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-framework-kms</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-framework-config</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-utils</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.crypto.tink</groupId>
<artifactId>tink</artifactId>
<version>${cs.tink.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,390 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.kms.provider;
import com.google.crypto.tink.subtle.AesGcmJce;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.framework.config.impl.ConfigurationVO;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.framework.kms.KMSProvider;
import org.apache.cloudstack.framework.kms.KeyPurpose;
import org.apache.cloudstack.framework.kms.WrappedKey;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Database-backed KMS provider that stores master KEKs encrypted in the configuration table.
* Uses AES-256-GCM for all cryptographic operations.
* <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.
*/
public class DatabaseKMSProvider implements KMSProvider {
// Configuration keys
public static final ConfigKey<Boolean> CacheEnabled = new ConfigKey<>(
"Advanced",
Boolean.class,
"kms.database.cache.enabled",
"true",
"Enable in-memory caching of KEKs for better performance",
true,
ConfigKey.Scope.Global
);
private static final Logger logger = LogManager.getLogger(DatabaseKMSProvider.class);
private static final String PROVIDER_NAME = "database";
private static final String KEK_CONFIG_PREFIX = "kms.database.kek.";
private static final int GCM_IV_LENGTH = 12; // 96 bits recommended for GCM
private static final int GCM_TAG_LENGTH = 16; // 128 bits
private static final String ALGORITHM = "AES/GCM/NoPadding";
// In-memory cache of KEKs (encrypted form cached, decrypted on demand)
private final Map<String, byte[]> kekCache = new ConcurrentHashMap<>();
private final SecureRandom secureRandom = new SecureRandom();
@Inject
private ConfigurationDao configDao;
@Override
public String getProviderName() {
return PROVIDER_NAME;
}
@Override
public String createKek(KeyPurpose purpose, String label, int keyBits) throws KMSException {
if (keyBits != 128 && keyBits != 192 && keyBits != 256) {
throw KMSException.invalidParameter("Key size must be 128, 192, or 256 bits");
}
if (StringUtils.isEmpty(label)) {
label = generateKekLabel(purpose);
}
String configKey = buildConfigKey(label);
// Check if KEK already exists
ConfigurationVO existing = configDao.findByName(configKey);
if (existing != null) {
throw KMSException.keyAlreadyExists("KEK with label " + label + " already exists");
}
try {
// Generate random KEK
byte[] kekBytes = new byte[keyBits / 8];
secureRandom.nextBytes(kekBytes);
// Store in configuration table (will be encrypted automatically due to "Secure" category)
String kekBase64 = java.util.Base64.getEncoder().encodeToString(kekBytes);
ConfigurationVO config = new ConfigurationVO(
"Secure", // Category - triggers encryption
"DEFAULT",
getConfigComponentName(),
configKey,
kekBase64,
"KMS KEK for " + purpose.getName() + " (label: " + label + ")"
);
configDao.persist(config);
// Cache the KEK
if (CacheEnabled.value()) {
kekCache.put(label, kekBytes);
}
logger.info("Created KEK with label {} for purpose {}", label, purpose);
return label;
} catch (Exception e) {
throw KMSException.kekOperationFailed("Failed to create KEK: " + e.getMessage(), e);
}
}
@Override
public String getConfigComponentName() {
return DatabaseKMSProvider.class.getSimpleName();
}
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[]{
CacheEnabled
};
}
@Override
public void deleteKek(String kekId) throws KMSException {
String configKey = buildConfigKey(kekId);
ConfigurationVO config = configDao.findByName(configKey);
if (config == null) {
throw KMSException.kekNotFound("KEK with label " + kekId + " not found");
}
try {
// Remove from configuration (name is the primary key)
configDao.remove(config.getName());
// Remove from cache
byte[] cachedKek = kekCache.remove(kekId);
if (cachedKek != null) {
Arrays.fill(cachedKek, (byte) 0); // Zeroize
}
logger.warn("Deleted KEK with label {}. All DEKs wrapped with this KEK are now unrecoverable!", kekId);
} catch (Exception e) {
throw KMSException.kekOperationFailed("Failed to delete KEK: " + e.getMessage(), e);
}
}
@Override
public List<String> listKeks(KeyPurpose purpose) throws KMSException {
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);
// Return keys from cache
for (String label : kekCache.keySet()) {
if (purpose == null || label.startsWith(purpose.getName())) {
keks.add(label);
}
}
return keks;
} catch (Exception e) {
throw KMSException.kekOperationFailed("Failed to list KEKs: " + e.getMessage(), e);
}
}
@Override
public boolean isKekAvailable(String kekId) throws KMSException {
try {
String configKey = buildConfigKey(kekId);
ConfigurationVO config = configDao.findByName(configKey);
return config != null && config.getValue() != null;
} catch (Exception e) {
logger.warn("Error checking KEK availability: {}", e.getMessage());
return false;
}
}
@Override
public WrappedKey wrapKey(byte[] plainKey, KeyPurpose purpose, String kekLabel) throws KMSException {
if (plainKey == null || plainKey.length == 0) {
throw KMSException.invalidParameter("Plain key cannot be null or empty");
}
byte[] kekBytes = loadKek(kekLabel);
try {
// Create AES-GCM cipher with the KEK
// Tink's AesGcmJce automatically generates a random IV and prepends it to the ciphertext
AesGcmJce aesgcm = new AesGcmJce(kekBytes);
// Encrypt the DEK (Tink's encrypt returns [IV][ciphertext+tag] format)
byte[] wrappedBlob = aesgcm.encrypt(plainKey, new byte[0]); // Empty associated data
WrappedKey wrapped = new WrappedKey(
kekLabel,
purpose,
ALGORITHM,
wrappedBlob,
PROVIDER_NAME,
new Date(),
null // zoneId set by caller
);
logger.debug("Wrapped {} key with KEK {}", purpose, kekLabel);
return wrapped;
} catch (Exception e) {
throw KMSException.wrapUnwrapFailed("Failed to wrap key: " + e.getMessage(), e);
} finally {
// Zeroize KEK
Arrays.fill(kekBytes, (byte) 0);
}
}
@Override
public byte[] unwrapKey(WrappedKey wrappedKey) throws KMSException {
if (wrappedKey == null) {
throw KMSException.invalidParameter("Wrapped key cannot be null");
}
byte[] kekBytes = loadKek(wrappedKey.getKekId());
try {
// Create AES-GCM cipher with the KEK
AesGcmJce aesgcm = new AesGcmJce(kekBytes);
// Tink's decrypt expects [IV][ciphertext+tag] format (same as encrypt returns)
byte[] blob = wrappedKey.getWrappedKeyMaterial();
if (blob.length < GCM_IV_LENGTH + GCM_TAG_LENGTH) {
throw new KMSException(KMSException.ErrorType.WRAP_UNWRAP_FAILED,
"Invalid wrapped key format: too short");
}
// Decrypt the DEK (Tink extracts IV from the blob automatically)
byte[] plainKey = aesgcm.decrypt(blob, new byte[0]); // Empty associated data
logger.debug("Unwrapped {} key with KEK {}", wrappedKey.getPurpose(), wrappedKey.getKekId());
return plainKey;
} catch (KMSException e) {
throw e;
} catch (Exception e) {
throw KMSException.wrapUnwrapFailed("Failed to unwrap key: " + e.getMessage(), e);
} finally {
// Zeroize KEK
Arrays.fill(kekBytes, (byte) 0);
}
}
@Override
public WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits) throws KMSException {
if (keyBits != 128 && keyBits != 192 && keyBits != 256) {
throw KMSException.invalidParameter("DEK size must be 128, 192, or 256 bits");
}
// Generate random DEK
byte[] dekBytes = new byte[keyBits / 8];
secureRandom.nextBytes(dekBytes);
try {
return wrapKey(dekBytes, purpose, kekLabel);
} finally {
// Zeroize DEK (wrapped version is in WrappedKey)
Arrays.fill(dekBytes, (byte) 0);
}
}
@Override
public WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel) throws KMSException {
// Unwrap with old KEK
byte[] plainKey = unwrapKey(oldWrappedKey);
try {
// Wrap with new KEK
return wrapKey(plainKey, oldWrappedKey.getPurpose(), newKekLabel);
} finally {
// Zeroize plaintext DEK
Arrays.fill(plainKey, (byte) 0);
}
}
@Override
public boolean healthCheck() throws KMSException {
try {
// Verify we can access configuration
if (configDao == null) {
logger.error("Configuration DAO is not initialized");
return false;
}
// Try to list KEKs (lightweight operation)
List<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) {
throw KMSException.healthCheckFailed("Health check failed: " + e.getMessage(), e);
}
}
// ==================== Private Helper Methods ====================
private byte[] loadKek(String kekLabel) throws KMSException {
// Check cache first
if (CacheEnabled.value()) {
byte[] cached = kekCache.get(kekLabel);
if (cached != null) {
return Arrays.copyOf(cached, cached.length); // Return copy
}
}
// Load from database
String configKey = buildConfigKey(kekLabel);
ConfigurationVO config = configDao.findByName(configKey);
if (config == null) {
throw KMSException.kekNotFound("KEK with label " + kekLabel + " not found");
}
try {
// getValue() automatically decrypts
String kekBase64 = config.getValue();
if (StringUtils.isEmpty(kekBase64)) {
throw KMSException.kekNotFound("KEK value is empty for label " + kekLabel);
}
byte[] kekBytes = java.util.Base64.getDecoder().decode(kekBase64);
// Cache for future use
if (CacheEnabled.value()) {
kekCache.put(kekLabel, Arrays.copyOf(kekBytes, kekBytes.length));
}
return kekBytes;
} catch (IllegalArgumentException e) {
throw KMSException.kekOperationFailed("Invalid KEK encoding for label " + kekLabel, e);
}
}
private String buildConfigKey(String label) {
return KEK_CONFIG_PREFIX + label;
}
private String generateKekLabel(KeyPurpose purpose) {
return purpose.getName() + "-kek-" + UUID.randomUUID().toString().substring(0, 8);
}
}

View File

@ -0,0 +1,20 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
name=database-kms
parent=kmsProvidersRegistry

View File

@ -0,0 +1,36 @@
<!--
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"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
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) -->
<bean id="databaseKMSProvider" class="org.apache.cloudstack.kms.provider.DatabaseKMSProvider">
<property name="name" value="DatabaseKMSProvider" />
</bean>
</beans>

39
plugins/kms/pom.xml Normal file
View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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>
<packaging>pom</packaging>
<name>Apache CloudStack Plugin - KMS</name>
<description>Key Management Service providers</description>
<parent>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloudstack-plugins</artifactId>
<version>4.23.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modules>
<module>database</module>
</modules>
</project>

View File

@ -97,6 +97,8 @@
<module>integrations/prometheus</module>
<module>integrations/kubernetes-service</module>
<module>kms</module>
<module>metrics</module>
<module>network-elements/bigswitch</module>

View File

@ -69,6 +69,11 @@
<artifactId>cloud-framework-ca</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-framework-kms</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-framework-jobs</artifactId>

View File

@ -115,6 +115,7 @@ import org.apache.cloudstack.api.response.HypervisorGuestOsResponse;
import org.apache.cloudstack.api.response.IPAddressResponse;
import org.apache.cloudstack.api.response.ImageStoreResponse;
import org.apache.cloudstack.api.response.InstanceGroupResponse;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.api.response.InternalLoadBalancerElementResponse;
import org.apache.cloudstack.api.response.IpForwardingRuleResponse;
import org.apache.cloudstack.api.response.IpQuarantineResponse;
@ -424,6 +425,7 @@ import com.cloud.user.AccountManager;
import com.cloud.user.SSHKeyPair;
import com.cloud.user.User;
import com.cloud.user.UserAccount;
import org.apache.cloudstack.kms.KMSKey;
import com.cloud.user.UserData;
import com.cloud.user.UserStatisticsVO;
import com.cloud.user.dao.UserDataDao;
@ -5802,4 +5804,48 @@ protected Map<String, ResourceIcon> getResourceIconsUsingOsCategory(List<Templat
response.setResponses(permissionResponses);
return response;
}
@Override
public KMSKeyResponse createKMSKeyResponse(KMSKey kmsKey) {
KMSKeyResponse response = new KMSKeyResponse();
response.setId(kmsKey.getUuid());
response.setName(kmsKey.getName());
response.setDescription(kmsKey.getDescription());
response.setPurpose(kmsKey.getPurpose().getName());
response.setAccountId(String.valueOf(kmsKey.getAccountId()));
response.setDomainId(String.valueOf(kmsKey.getDomainId()));
response.setZoneId(String.valueOf(kmsKey.getZoneId()));
response.setProvider(kmsKey.getProviderName());
response.setAlgorithm(kmsKey.getAlgorithm());
response.setKeyBits(kmsKey.getKeyBits());
response.setState(kmsKey.getState().toString());
response.setCreated(kmsKey.getCreated());
// Set account name
Account account = ApiDBUtils.findAccountById(kmsKey.getAccountId());
if (account != null) {
response.setAccountName(account.getAccountName());
}
// Set domain name
Domain domain = ApiDBUtils.findDomainById(kmsKey.getDomainId());
if (domain != null) {
response.setDomainName(domain.getName());
}
// Set zone name
DataCenter zone = ApiDBUtils.findZoneById(kmsKey.getZoneId());
if (zone != null) {
response.setZoneName(zone.getName());
}
// Set KEK label (admin only)
Account caller = CallContext.current().getCallingAccount();
if (caller != null && (caller.getType() == Account.Type.ADMIN || caller.getType() == Account.Type.RESOURCE_DOMAIN_ADMIN)) {
response.setKekLabel(kmsKey.getKekLabel());
}
response.setObjectName("kmskey");
return response;
}
}

View File

@ -0,0 +1,910 @@
// 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;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.component.PluggableService;
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.user.kms.CreateKMSKeyCmd;
import org.apache.cloudstack.api.command.user.kms.DeleteKMSKeyCmd;
import org.apache.cloudstack.api.command.user.kms.ListKMSKeysCmd;
import org.apache.cloudstack.api.command.user.kms.UpdateKMSKeyCmd;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.ConfigKey;
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.dao.KMSKekVersionDao;
import org.apache.cloudstack.kms.dao.KMSKeyDao;
import org.apache.cloudstack.kms.dao.KMSWrappedKeyDao;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import java.util.ArrayList;
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<>();
private static KMSProvider configuredKmsProvider;
@Inject
private KMSWrappedKeyDao kmsWrappedKeyDao;
@Inject
private KMSKeyDao kmsKeyDao;
@Inject
private KMSKekVersionDao kmsKekVersionDao;
@Inject
private AccountManager accountManager;
@Inject
private ResponseGenerator responseGenerator;
private List<KMSProvider> kmsProviders;
// ==================== Provider Management ====================
@Override
public List<? extends KMSProvider> listKMSProviders() {
return kmsProviders;
}
@Override
public KMSProvider getKMSProvider(String name) {
if (StringUtils.isEmpty(name)) {
return getConfiguredKmsProvider();
}
String providerName = name.toLowerCase();
if (!kmsProviderMap.containsKey(providerName)) {
throw new CloudRuntimeException(String.format("KMS provider '%s' not found", providerName));
}
KMSProvider provider = kmsProviderMap.get(providerName);
if (provider == null) {
throw new CloudRuntimeException(String.format("KMS provider '%s' returned is null", providerName));
}
return provider;
}
@Override
public KMSProvider getKMSProviderForZone(Long zoneId) throws KMSException {
// For now, use global provider
// In future, could support zone-specific providers via zone-scoped config
return getConfiguredKmsProvider();
}
@Override
public boolean isKmsEnabled(Long zoneId) {
if (zoneId == null) {
return false;
}
return KMSEnabled.valueIn(zoneId);
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_KMS_KEK_CREATE, eventDescription = "creating KEK", async = false)
public String createKek(Long zoneId, KeyPurpose purpose, String label, int keyBits) throws KMSException {
validateKmsEnabled(zoneId);
KMSProvider provider = getKMSProviderForZone(zoneId);
try {
logger.info("Creating KEK for zone {} with purpose {} and {} bits", zoneId, purpose, keyBits);
return retryOperation(() -> provider.createKek(purpose, label, keyBits));
} catch (Exception e) {
logger.error("Failed to create KEK for zone {}: {}", zoneId, e.getMessage());
throw handleKmsException(e);
}
}
// ==================== KEK Management ====================
@Override
@ActionEvent(eventType = EventTypes.EVENT_KMS_KEK_DELETE, eventDescription = "deleting KEK", async = false)
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);
try {
logger.warn("Deleting KEK {} for zone {}", kekId, zoneId);
retryOperation(() -> {
provider.deleteKek(kekId);
return null;
});
} catch (Exception e) {
logger.error("Failed to delete KEK {} for zone {}: {}", kekId, zoneId, e.getMessage());
throw handleKmsException(e);
}
}
@Override
public List<String> listKeks(Long zoneId, KeyPurpose purpose) throws KMSException {
validateKmsEnabled(zoneId);
KMSProvider provider = getKMSProviderForZone(zoneId);
try {
return retryOperation(() -> provider.listKeks(purpose));
} catch (Exception e) {
logger.error("Failed to list KEKs for zone {}: {}", zoneId, e.getMessage());
throw handleKmsException(e);
}
}
@Override
public boolean isKekAvailable(Long zoneId, String kekId) throws KMSException {
if (!isKmsEnabled(zoneId)) {
return false;
}
try {
KMSProvider provider = getKMSProviderForZone(zoneId);
return provider.isKekAvailable(kekId);
} catch (Exception e) {
logger.warn("Error checking KEK availability: {}", e.getMessage());
return false;
}
}
@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);
if (StringUtils.isEmpty(oldKekLabel)) {
throw KMSException.invalidParameter("oldKekLabel must be specified");
}
KMSProvider provider = getKMSProviderForZone(zoneId);
try {
logger.info("Starting KEK rotation from {} to {} for zone {} and purpose {}",
oldKekLabel, newKekLabel, zoneId, purpose);
// Find KMS key by old KEK label
KMSKeyVO kmsKey = kmsKeyDao.findByKekLabel(oldKekLabel, provider.getProviderName());
if (kmsKey == null) {
throw KMSException.kekNotFound("KMS key not found for KEK label: " + oldKekLabel);
}
// Generate new KEK label if not provided
if (StringUtils.isEmpty(newKekLabel)) {
newKekLabel = purpose.getName() + "-kek-" + UUID.randomUUID().toString().substring(0, 8);
}
// Create new KEK in provider
String newKekId = provider.createKek(purpose, newKekLabel, keyBits);
// Create new KEK version (marks old as Previous, new as Active)
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(),
newVersion.getVersionNumber() - 1);
// TODO: Schedule background job to rewrap all DEKs (Phase 5)
// This will gradually rewrap wrapped keys to use the new KEK version
return newKekId;
} catch (Exception e) {
logger.error("KEK rotation failed for zone {}: {}", zoneId, e.getMessage());
throw handleKmsException(e);
}
}
// ==================== DEK Operations ====================
@Override
@ActionEvent(eventType = EventTypes.EVENT_KMS_KEY_UNWRAP, eventDescription = "unwrapping volume key", async = false)
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);
try {
logger.debug("Unwrapping {} key", wrappedKey.getPurpose());
return retryOperation(() -> provider.unwrapKey(wrappedKey));
} catch (Exception e) {
logger.error("Failed to unwrap key: {}", e.getMessage());
throw handleKmsException(e);
}
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_KMS_HEALTH_CHECK, eventDescription = "KMS health check", async = false)
public boolean healthCheck(Long zoneId) throws KMSException {
if (!isKmsEnabled(zoneId)) {
logger.debug("KMS is not enabled for zone {}", zoneId);
return false;
}
try {
KMSProvider provider = getKMSProviderForZone(zoneId);
return provider.healthCheck();
} catch (Exception e) {
logger.error("Health check failed for zone {}: {}", zoneId, e.getMessage());
throw handleKmsException(e);
}
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_KMS_KEK_CREATE, eventDescription = "creating user KMS key", async = false)
public KMSKey createUserKMSKey(Long accountId, Long domainId, Long zoneId,
String name, String description, KeyPurpose purpose,
Integer keyBits) throws KMSException {
validateKmsEnabled(zoneId);
KMSProvider provider = getKMSProviderForZone(zoneId);
// Generate unique KEK label
String kekLabel = purpose.getName() + "-kek-" + UUID.randomUUID().toString().substring(0, 8);
// Create KEK in provider
String providerKekLabel;
try {
providerKekLabel = retryOperation(() -> provider.createKek(purpose, kekLabel, keyBits));
} catch (Exception e) {
throw handleKmsException(e);
}
// Create metadata entry
KMSKeyVO kmsKey = new KMSKeyVO(name, description, providerKekLabel, purpose,
accountId, domainId, zoneId, provider.getProviderName(),
"AES/GCM/NoPadding", keyBits);
kmsKey = kmsKeyDao.persist(kmsKey);
// Create initial KEK version (version 1, status=Active)
KMSKekVersionVO initialVersion = new KMSKekVersionVO(kmsKey.getId(), 1, providerKekLabel,
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);
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)) {
return null;
}
return key;
}
// ==================== Helper Methods ====================
@Override
public boolean hasPermission(Long callerAccountId, String keyUuid) {
KMSKeyVO key = kmsKeyDao.findByUuid(keyUuid);
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;
}
@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);
}
// Check permission
if (!hasPermission(callerAccountId, uuid)) {
throw KMSException.invalidParameter("No permission to delete KMS key: " + uuid);
}
// Check if key is in use
long wrappedKeyCount = kmsKeyDao.countWrappedKeysByKmsKey(key.getId());
if (wrappedKeyCount > 0) {
throw KMSException.invalidParameter("Cannot delete KMS key: " + wrappedKeyCount +
" wrapped key(s) still reference this key");
}
// Soft delete
key.setState(KMSKey.State.Deleted);
key.setRemoved(new java.util.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);
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_KMS_KEK_CREATE, eventDescription = "updating user KMS key", async = false)
public KMSKey updateUserKMSKey(String uuid, 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);
}
boolean updated = false;
if (name != null && !name.equals(key.getName())) {
key.setName(name);
updated = true;
}
if (description != null && !description.equals(key.getDescription())) {
key.setDescription(description);
updated = true;
}
if (state != null && state != key.getState()) {
if (state == KMSKey.State.Deleted) {
throw KMSException.invalidParameter("Cannot set state to Deleted. Use deleteKMSKey instead.");
}
key.setState(state);
updated = true;
}
if (updated) {
kmsKeyDao.update(key.getId(), key);
logger.info("Updated KMS key '{}' (UUID: {})", key.getName(), uuid);
}
return key;
}
/**
* Unwrap a DEK by wrapped key ID, trying multiple KEK versions if needed
*/
@Override
public byte[] unwrapKey(Long wrappedKeyId) throws KMSException {
KMSWrappedKeyVO wrappedVO = kmsWrappedKeyDao.findById(wrappedKeyId);
if (wrappedVO == null) {
throw KMSException.kekNotFound("Wrapped key not found: " + wrappedKeyId);
}
KMSKeyVO kmsKey = kmsKeyDao.findById(wrappedVO.getKmsKeyId());
if (kmsKey == null) {
throw KMSException.kekNotFound("KMS key not found for wrapped key: " + wrappedKeyId);
}
KMSProvider provider = getKMSProvider(kmsKey.getProviderName());
// Try the specific version first if available
if (wrappedVO.getKekVersionId() != null) {
KMSKekVersionVO version = kmsKekVersionDao.findById(wrappedVO.getKekVersionId());
if (version != null && version.getStatus() != KMSKekVersionVO.Status.Archived) {
try {
WrappedKey wrapped = new WrappedKey(version.getKekLabel(), kmsKey.getPurpose(),
kmsKey.getAlgorithm(), wrappedVO.getWrappedBlob(),
kmsKey.getProviderName(), wrappedVO.getCreated(), kmsKey.getZoneId());
byte[] dek = retryOperation(() -> provider.unwrapKey(wrapped));
logger.debug("Successfully unwrapped key {} with KEK version {}", wrappedKeyId,
version.getVersionNumber());
return dek;
} catch (Exception e) {
logger.warn("Failed to unwrap with version {}: {}", version.getVersionNumber(), e.getMessage());
}
}
}
// Fallback: try all available versions for decryption
List<KMSKekVersionVO> versions = getKekVersionsForDecryption(kmsKey.getId());
for (KMSKekVersionVO version : versions) {
try {
WrappedKey wrapped = new WrappedKey(version.getKekLabel(), kmsKey.getPurpose(),
kmsKey.getAlgorithm(), wrappedVO.getWrappedBlob(),
kmsKey.getProviderName(), wrappedVO.getCreated(), kmsKey.getZoneId());
byte[] dek = retryOperation(() -> provider.unwrapKey(wrapped));
logger.info("Successfully unwrapped key {} with KEK version {} (fallback)", wrappedKeyId,
version.getVersionNumber());
return dek;
} catch (Exception e) {
logger.debug("Failed to unwrap with version {}: {}", version.getVersionNumber(), e.getMessage());
}
}
throw KMSException.wrapUnwrapFailed("Failed to unwrap key with any available KEK version");
}
// ==================== 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 {
// Get and validate KMS key
KMSKey kmsKey = getUserKMSKey(kekUuid, callerAccountId);
if (kmsKey == null) {
throw KMSException.kekNotFound("KMS key not found or no permission: " + kekUuid);
}
if (kmsKey.getState() != KMSKey.State.Enabled) {
throw KMSException.invalidParameter("KMS key is not enabled: " + kekUuid);
}
if (kmsKey.getPurpose() != KeyPurpose.VOLUME_ENCRYPTION) {
throw KMSException.invalidParameter("KMS key purpose is not VOLUME_ENCRYPTION: " + kekUuid);
}
// Get provider
KMSProvider provider = getKMSProviderForZone(kmsKey.getZoneId());
// Get active KEK version
KMSKekVersionVO activeVersion = getActiveKekVersion(kmsKey.getId());
// Generate and wrap DEK using active KEK version
int dekSize = KMSDekSizeBits.value();
WrappedKey wrappedKey;
try {
wrappedKey = retryOperation(() ->
provider.generateAndWrapDek(KeyPurpose.VOLUME_ENCRYPTION, activeVersion.getKekLabel(), dekSize));
// Store the wrapped key in database
KMSWrappedKeyVO wrappedKeyVO = new KMSWrappedKeyVO(kmsKey.getId(), activeVersion.getId(),
kmsKey.getZoneId(), wrappedKey.getWrappedKeyMaterial());
wrappedKeyVO = kmsWrappedKeyDao.persist(wrappedKeyVO);
// 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(
wrappedKeyVO.getUuid(),
wrappedKey.getKekId(),
wrappedKey.getPurpose(),
wrappedKey.getAlgorithm(),
wrappedKey.getWrappedKeyMaterial(),
wrappedKey.getProviderName(),
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());
return wrappedKey;
}
/**
* Get the active KEK version for a KMS key
*/
private KMSKekVersionVO getActiveKekVersion(Long kmsKeyId) throws KMSException {
KMSKekVersionVO activeVersion = kmsKekVersionDao.getActiveVersion(kmsKeyId);
if (activeVersion == null) {
throw KMSException.kekNotFound("No active KEK version found for KMS key ID: " + kmsKeyId);
}
return activeVersion;
}
// ==================== Configurable Implementation ====================
@Override
public KMSKeyResponse createKMSKey(CreateKMSKeyCmd cmd) throws KMSException {
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()) &&
!accountManager.isDomainAdmin(caller.getId())) {
throw new ServerApiException(ApiErrorCode.UNAUTHORIZED,
"Only admins and domain admins can create keys for other accounts");
}
if (cmd.getAccountName() != null && cmd.getDomainId() != null) {
targetAccount = accountManager.getActiveAccountByName(cmd.getAccountName(), cmd.getDomainId());
if (targetAccount == null) {
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");
}
}
// Validate purpose
KeyPurpose keyPurpose;
try {
keyPurpose = KeyPurpose.fromString(cmd.getPurpose());
} catch (IllegalArgumentException e) {
throw KMSException.invalidParameter("Invalid purpose: " + cmd.getPurpose() +
". Valid values: VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET");
}
// Validate key bits
int bits = cmd.getKeyBits();
if (bits != 128 && bits != 192 && bits != 256) {
throw KMSException.invalidParameter("Key bits must be 128, 192, or 256");
}
// Create the KMS key
KMSKey kmsKey = createUserKMSKey(
targetAccount.getId(),
targetAccount.getDomainId(),
cmd.getZoneId(),
cmd.getName(),
cmd.getDescription(),
keyPurpose,
bits
);
return responseGenerator.createKMSKeyResponse(kmsKey);
}
// ==================== KEK Version Management ====================
@Override
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;
}
// 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
}
}
// 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
}
}
// 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
ListResponse<KMSKeyResponse> listResponse = new ListResponse<>();
listResponse.setResponses(new java.util.ArrayList<>(), 0);
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;
}
// List accessible keys
List<? extends KMSKey> keys = listUserKMSKeys(
caller.getId(),
caller.getDomainId(),
cmd.getZoneId(),
keyPurpose,
keyState
);
List<KMSKeyResponse> responses = new java.util.ArrayList<>();
for (KMSKey key : keys) {
responses.add(responseGenerator.createKMSKeyResponse(key));
}
ListResponse<KMSKeyResponse> listResponse = new ListResponse<>();
listResponse.setResponses(responses, responses.size());
return listResponse;
}
@Override
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");
}
}
// 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,
cmd.getName(), cmd.getDescription(), keyState);
return responseGenerator.createKMSKeyResponse(updatedKey);
}
@Override
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;
}
// ==================== User KEK Management ====================
/**
* Create a new KEK version for a KMS key
*/
private KMSKekVersionVO createKekVersion(Long kmsKeyId, String kekLabel, int keyBits) throws KMSException {
// Get existing versions to determine next version number
List<KMSKekVersionVO> existingVersions = kmsKekVersionDao.listByKmsKeyId(kmsKeyId);
int nextVersion = existingVersions.stream()
.mapToInt(KMSKekVersionVO::getVersionNumber)
.max()
.orElse(0) + 1;
// Mark current active version as Previous
KMSKekVersionVO currentActive = kmsKekVersionDao.getActiveVersion(kmsKeyId);
if (currentActive != null) {
currentActive.setStatus(KMSKekVersionVO.Status.Previous);
kmsKekVersionDao.update(currentActive.getId(), currentActive);
}
// Create new active version
KMSKekVersionVO newVersion = new KMSKekVersionVO(kmsKeyId, nextVersion, kekLabel,
KMSKekVersionVO.Status.Active);
newVersion = kmsKekVersionDao.persist(newVersion);
logger.info("Created KEK version {} for KMS key {} (label: {})", nextVersion, kmsKeyId, kekLabel);
return newVersion;
}
private void validateKmsEnabled(Long zoneId) throws KMSException {
if (zoneId == null) {
throw KMSException.invalidParameter("Zone ID cannot be null");
}
if (!isKmsEnabled(zoneId)) {
throw KMSException.providerNotInitialized(
"KMS is not enabled for zone " + zoneId + ". Set kms.enabled=true for this zone.");
}
}
private <T> T retryOperation(KmsOperation<T> operation) throws Exception {
int maxRetries = KMSRetryCount.value();
int retryDelay = KMSRetryDelayMs.value();
Exception lastException = null;
for (int attempt = 0; attempt <= maxRetries; attempt++) {
try {
return operation.execute();
} catch (Exception e) {
lastException = e;
// Check if retryable
if (e instanceof KMSException && !((KMSException) e).isRetryable()) {
throw e;
}
if (attempt < maxRetries) {
logger.warn("KMS operation failed (attempt {}/{}): {}. Retrying...",
attempt + 1, maxRetries + 1, e.getMessage());
try {
Thread.sleep((long) retryDelay * (attempt + 1)); // Exponential backoff
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new CloudRuntimeException("Interrupted during retry", ie);
}
} else {
logger.error("KMS operation failed after {} attempts", maxRetries + 1);
}
}
}
if (lastException != null) {
throw lastException;
}
throw new CloudRuntimeException("KMS operation failed with no exception details");
}
private KMSException handleKmsException(Exception e) {
if (e instanceof KMSException) {
return (KMSException) e;
}
return KMSException.transientError("KMS operation failed: " + e.getMessage(), e);
}
private KMSProvider getConfiguredKmsProvider() {
if (configuredKmsProvider != null) {
return configuredKmsProvider;
}
String providerName = KMSProviderPlugin.value();
if (kmsProviderMap.containsKey(providerName) && kmsProviderMap.get(providerName) != null) {
configuredKmsProvider = kmsProviderMap.get(providerName);
return configuredKmsProvider;
}
throw new CloudRuntimeException("Failed to find default configured KMS provider plugin: " + providerName);
}
public void setKmsProviders(List<KMSProvider> kmsProviders) {
this.kmsProviders = kmsProviders;
initializeKmsProviderMap();
}
// ==================== API Response Methods ====================
private void initializeKmsProviderMap() {
if (kmsProviderMap != null && kmsProviderMap.size() != kmsProviders.size()) {
for (KMSProvider provider : kmsProviders) {
kmsProviderMap.put(provider.getProviderName().toLowerCase(), provider);
logger.info("Registered KMS provider: {}", provider.getProviderName());
}
}
}
@Override
public boolean start() {
super.start();
initializeKmsProviderMap();
String configuredProviderName = KMSProviderPlugin.value();
if (kmsProviderMap.containsKey(configuredProviderName)) {
configuredKmsProvider = kmsProviderMap.get(configuredProviderName);
logger.info("Configured KMS provider: {}", configuredKmsProvider.getProviderName());
}
if (configuredKmsProvider == null) {
logger.warn("No valid configured KMS provider found. KMS functionality will be unavailable.");
// Don't fail - KMS is optional
return true;
}
// Run health check on startup
try {
boolean healthy = configuredKmsProvider.healthCheck();
if (healthy) {
logger.info("KMS provider {} health check passed", configuredKmsProvider.getProviderName());
} else {
logger.warn("KMS provider {} health check failed", configuredKmsProvider.getProviderName());
}
} catch (Exception e) {
logger.warn("KMS provider health check error: {}", e.getMessage());
}
return true;
}
@Override
public String getConfigComponentName() {
return KMSManager.class.getSimpleName();
}
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[]{
KMSProviderPlugin,
KMSEnabled,
KMSDekSizeBits,
KMSRetryCount,
KMSRetryDelayMs,
KMSOperationTimeoutSec
};
}
@Override
public List<Class<?>> getCommands() {
List<Class<?>> cmdList = new ArrayList<>();
cmdList.add(ListKMSKeysCmd.class);
cmdList.add(CreateKMSKeyCmd.class);
cmdList.add(UpdateKMSKeyCmd.class);
cmdList.add(DeleteKMSKeyCmd.class);
return cmdList;
}
@FunctionalInterface
private interface KmsOperation<T> {
T execute() throws Exception;
}
}

View File

@ -330,6 +330,11 @@
<property name="caProviders" value="#{caProvidersRegistry.registered}" />
</bean>
<!-- KMS manager -->
<bean id="kmsManager" class="org.apache.cloudstack.kms.KMSManagerImpl">
<property name="kmsProviders" value="#{kmsProvidersRegistry.registered}" />
</bean>
<bean id="annotationService" class="org.apache.cloudstack.annotation.AnnotationManagerImpl">
<property name="kubernetesServiceHelpers" value="#{kubernetesServiceHelperRegistry.registered}" />
</bean>

View File

@ -56,6 +56,7 @@ known_categories = {
'HypervisorGuestOsNames': 'Guest OS',
'Domain': 'Domain',
'Template': 'Template',
'KMS': 'KMS',
'Iso': 'ISO',
'Volume': 'Volume',
'Vlan': 'VLAN',