diff --git a/compute/src/main/java/org/zstack/compute/vm/VmAllocateVolumeFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmAllocateVolumeFlow.java index bb21cd4a0e5..e11ea846195 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmAllocateVolumeFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmAllocateVolumeFlow.java @@ -118,6 +118,8 @@ protected List prepareMsg(Map ctx) { if (disk != null && !isEmpty(disk.getSystemTags())) { tags.addAll(disk.getSystemTags()); } + Boolean volEnc = disk != null ? disk.getEncrypted() : false; + msg.setEncrypted(volEnc); } else if (vspec.isData()) { DiskAO disk = isEmpty(spec.getDataDisks()) ? null : spec.getDataDisks().size() > dataVolumeIndex ? spec.getDataDisks().get(dataVolumeIndex) : null; diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstantiateOtherDiskFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmInstantiateOtherDiskFlow.java index 69537bd55f3..d6dd34d38b6 100644 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstantiateOtherDiskFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstantiateOtherDiskFlow.java @@ -86,6 +86,7 @@ public void setup() { } else if (isAttachDataVolume()) { VolumeVO volume = Q.New(VolumeVO.class).eq(VolumeVO_.uuid, diskAO.getSourceUuid()).find(); volumeInventory = VolumeInventory.valueOf(volume); + setupEncryptExistingVolumeFlow(); setupAttachVolumeFlows(); } else if (diskAO.getSourceUuid() != null && diskAO.getSourceType() != null) { setupAttachOtherDiskFlows(); @@ -180,6 +181,7 @@ public void run(final FlowTrigger innerTrigger, Map data) { msg.setDiskOfferingUuid(diskAO.getDiskOfferingUuid()); msg.setPrimaryStorageUuid(allocatedPrimaryStorageUuid); msg.setDescription(String.format("vm-%s-data-volume", vmUuid)); + msg.setEncrypted(Boolean.TRUE.equals(diskAO.getEncrypted())); bus.makeLocalServiceId(msg, VolumeConstant.SERVICE_ID); bus.send(msg, new CloudBusCallBack(innerTrigger) { @Override @@ -328,6 +330,7 @@ public void run(final FlowTrigger innerTrigger, Map data) { } else { cmsg.setPrimaryStorageUuid(allocatedPrimaryStorageUuid[0]); } + cmsg.setEncrypted(Boolean.TRUE.equals(diskAO.getEncrypted())); bus.makeLocalServiceId(cmsg, VolumeConstant.SERVICE_ID); bus.send(cmsg, new CloudBusCallBack(innerTrigger) { @@ -404,6 +407,56 @@ public void run(MessageReply reply) { }); } + /** + * When the caller requested an encrypted data volume (DiskAO.encrypted=true) but the + * existing source volume is not yet encrypted, transition the source bits to LUKS + * in place before attaching. Delegates to {@code EncryptVolumeMsg} so the actual + * key/secret/PS-conversion logic lives in {@code VolumeBase} (shared with the + * create-data-volume-from-template flow). + * + *

Skipped when: + *

+ */ + private void setupEncryptExistingVolumeFlow() { + if (!Boolean.TRUE.equals(diskAO.getEncrypted())) { + return; + } + if (volumeInventory != null && Boolean.TRUE.equals(volumeInventory.getEncrypted())) { + return; + } + flow(new NoRollbackFlow() { + String __name__ = String.format("encrypt-existing-data-volume-%s-in-place", + diskAO.getSourceUuid()); + + @Override + public void run(final FlowTrigger innerTrigger, Map data) { + EncryptVolumeMsg emsg = new EncryptVolumeMsg(); + emsg.setVolumeUuid(volumeInventory.getUuid()); + emsg.setHostUuid(hostUuid); + emsg.setPurpose("attach-existing-disk-as-encrypted-data-volume"); + bus.makeTargetServiceIdByResourceUuid(emsg, VolumeConstant.SERVICE_ID, + volumeInventory.getUuid()); + bus.send(emsg, new CloudBusCallBack(innerTrigger) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + innerTrigger.fail(reply.getError()); + return; + } + EncryptVolumeReply er = reply.castReply(); + if (er.getInventory() != null) { + volumeInventory = er.getInventory(); + } + innerTrigger.next(); + } + }); + } + }); + } + private void setupAttachOtherDiskFlows() { flow(new NoRollbackFlow() { String __name__ = String.format("attach-other-Disk-to-vm-%s", vmUuid); diff --git a/conf/db/zsv/V5.1.0__schema.sql b/conf/db/zsv/V5.1.0__schema.sql index 0c2187e60bb..665dfec34aa 100644 --- a/conf/db/zsv/V5.1.0__schema.sql +++ b/conf/db/zsv/V5.1.0__schema.sql @@ -10,3 +10,15 @@ DELETE FROM `EncryptedResourceKeyRefVO` ALTER TABLE `EncryptedResourceKeyRefVO` ADD CONSTRAINT `fkEncryptedResourceKeyRefResourceVO` FOREIGN KEY (`resourceUuid`) REFERENCES `ResourceVO`(`uuid`) ON DELETE CASCADE; + +-- Volume LUKS encryption flag (API opt-in + EncryptedResourceKeyRefVO binding) + +ALTER TABLE `zstack`.`VolumeEO` ADD COLUMN `encrypted` tinyint(1) NOT NULL DEFAULT 0; + +DROP VIEW IF EXISTS `zstack`.`VolumeVO`; +CREATE VIEW `zstack`.`VolumeVO` AS +SELECT uuid, name, description, primaryStorageUuid, vmInstanceUuid, diskOfferingUuid, + rootImageUuid, installPath, type, status, size, actualSize, deviceId, format, state, createDate, lastOpDate, + isShareable, volumeQos, lastVmInstanceUuid, lastDetachDate, lastAttachDate, protocol, encrypted +FROM `zstack`.`VolumeEO` +WHERE deleted IS NULL; diff --git a/conf/springConfigXml/VolumeManager.xml b/conf/springConfigXml/VolumeManager.xml index 977134a8873..43feab624c5 100755 --- a/conf/springConfigXml/VolumeManager.xml +++ b/conf/springConfigXml/VolumeManager.xml @@ -92,4 +92,16 @@ + + + + + + + + + + + diff --git a/header/src/main/java/org/zstack/header/secret/SecretHostEnsureLuksSecretFileMsg.java b/header/src/main/java/org/zstack/header/secret/SecretHostEnsureLuksSecretFileMsg.java new file mode 100644 index 00000000000..425a35a3d15 --- /dev/null +++ b/header/src/main/java/org/zstack/header/secret/SecretHostEnsureLuksSecretFileMsg.java @@ -0,0 +1,28 @@ +package org.zstack.header.secret; + +import org.zstack.header.host.HostMessage; +import org.zstack.header.log.NoLogging; +import org.zstack.header.message.NeedReplyMessage; + +public class SecretHostEnsureLuksSecretFileMsg extends NeedReplyMessage implements HostMessage { + private String hostUuid; + @NoLogging + private String dekBase64; + + @Override + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public String getDekBase64() { + return dekBase64; + } + + public void setDekBase64(String dekBase64) { + this.dekBase64 = dekBase64; + } +} diff --git a/header/src/main/java/org/zstack/header/secret/SecretHostEnsureLuksSecretFileReply.java b/header/src/main/java/org/zstack/header/secret/SecretHostEnsureLuksSecretFileReply.java new file mode 100644 index 00000000000..129cc5cb2e5 --- /dev/null +++ b/header/src/main/java/org/zstack/header/secret/SecretHostEnsureLuksSecretFileReply.java @@ -0,0 +1,18 @@ +package org.zstack.header.secret; + +import org.zstack.header.message.MessageReply; + +public class SecretHostEnsureLuksSecretFileReply extends MessageReply { + public static final String ERROR_CODE_KEYS_NOT_ON_DISK = "KEY_AGENT_KEYS_NOT_ON_DISK"; + public static final String ERROR_CODE_KEY_FILES_INTEGRITY_MISMATCH = "KEY_AGENT_KEY_FILES_INTEGRITY_MISMATCH"; + + private String secFilePath; + + public String getSecFilePath() { + return secFilePath; + } + + public void setSecFilePath(String secFilePath) { + this.secFilePath = secFilePath; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/EncryptVolumeBitsOnPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/EncryptVolumeBitsOnPrimaryStorageMsg.java new file mode 100644 index 00000000000..297165fc17e --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/EncryptVolumeBitsOnPrimaryStorageMsg.java @@ -0,0 +1,61 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.NeedReplyMessage; + +/** + * Triggers an in-place LUKS encryption of an existing volume file on primary storage. + * Used after downloading a data-volume template's plain bits to LocalStorage when the + * volume is marked encrypted: the agent converts the plain qcow2/raw at {@link #installPath} + * into a LUKS-encrypted qcow2 (overwriting in place). + * + * The DEK is staged on the host out-of-band (caller stages the secret material file via + * SecretHostEnsureLuksSecretFileMsg and passes the file path here). + */ +public class EncryptVolumeBitsOnPrimaryStorageMsg extends NeedReplyMessage implements PrimaryStorageMessage { + private String primaryStorageUuid; + private String hostUuid; + private String volumeUuid; + private String installPath; + private String encryptLuksSecretMaterialFilePath; + + @Override + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public String getVolumeUuid() { + return volumeUuid; + } + + public void setVolumeUuid(String volumeUuid) { + this.volumeUuid = volumeUuid; + } + + public String getInstallPath() { + return installPath; + } + + public void setInstallPath(String installPath) { + this.installPath = installPath; + } + + public String getEncryptLuksSecretMaterialFilePath() { + return encryptLuksSecretMaterialFilePath; + } + + public void setEncryptLuksSecretMaterialFilePath(String encryptLuksSecretMaterialFilePath) { + this.encryptLuksSecretMaterialFilePath = encryptLuksSecretMaterialFilePath; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/EncryptVolumeBitsOnPrimaryStorageReply.java b/header/src/main/java/org/zstack/header/storage/primary/EncryptVolumeBitsOnPrimaryStorageReply.java new file mode 100644 index 00000000000..2410ced31e6 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/EncryptVolumeBitsOnPrimaryStorageReply.java @@ -0,0 +1,6 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.MessageReply; + +public class EncryptVolumeBitsOnPrimaryStorageReply extends MessageReply { +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/InstantiateVolumeOnPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/InstantiateVolumeOnPrimaryStorageMsg.java index ee0d3ae796f..d9306da2355 100755 --- a/header/src/main/java/org/zstack/header/storage/primary/InstantiateVolumeOnPrimaryStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/InstantiateVolumeOnPrimaryStorageMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.NeedReplyMessage; import org.zstack.header.message.ReplayableMessage; import org.zstack.header.volume.VolumeInventory; +import org.zstack.header.volume.VolumeLuksAgentSpec; public class InstantiateVolumeOnPrimaryStorageMsg extends NeedReplyMessage implements PrimaryStorageMessage, ReplayableMessage { private HostInventory destHost; @@ -11,6 +12,7 @@ public class InstantiateVolumeOnPrimaryStorageMsg extends NeedReplyMessage imple private String primaryStorageUuid; private boolean skipIfExisting; private String allocatedInstallUrl; + private VolumeLuksAgentSpec volumeLuksAgentSpec; public String getAllocatedInstallUrl() { return allocatedInstallUrl; @@ -53,6 +55,14 @@ public void setSkipIfExisting(boolean skipIfExisting) { this.skipIfExisting = skipIfExisting; } + public VolumeLuksAgentSpec getVolumeLuksAgentSpec() { + return volumeLuksAgentSpec; + } + + public void setVolumeLuksAgentSpec(VolumeLuksAgentSpec volumeLuksAgentSpec) { + this.volumeLuksAgentSpec = volumeLuksAgentSpec; + } + @Override public String getResourceUuid() { return volume.getUuid(); diff --git a/header/src/main/java/org/zstack/header/vm/DiskAO.java b/header/src/main/java/org/zstack/header/vm/DiskAO.java index 3677ff749f3..5762969adfa 100644 --- a/header/src/main/java/org/zstack/header/vm/DiskAO.java +++ b/header/src/main/java/org/zstack/header/vm/DiskAO.java @@ -25,6 +25,7 @@ public class DiskAO { private String sourceUuid; private List systemTags; private String name; + private Boolean encrypted; public DiskAO withImage(String imageUuid) { this.templateUuid = imageUuid; @@ -139,6 +140,14 @@ public void setName(String name) { this.name = name; } + public Boolean getEncrypted() { + return encrypted; + } + + public void setEncrypted(Boolean encrypted) { + this.encrypted = encrypted; + } + public static DiskAO rootDisk() { DiskAO disk = new DiskAO(); disk.setBoot(true); diff --git a/header/src/main/java/org/zstack/header/volume/CreateDataVolumeFromVolumeTemplateMsg.java b/header/src/main/java/org/zstack/header/volume/CreateDataVolumeFromVolumeTemplateMsg.java index 781c33bc9ba..06195687e39 100644 --- a/header/src/main/java/org/zstack/header/volume/CreateDataVolumeFromVolumeTemplateMsg.java +++ b/header/src/main/java/org/zstack/header/volume/CreateDataVolumeFromVolumeTemplateMsg.java @@ -18,6 +18,7 @@ public class CreateDataVolumeFromVolumeTemplateMsg extends NeedReplyMessage impl private String hostUuid; private String resourceUuid; private String accountUuid; + private Boolean encrypted; private APICreateDataVolumeFromVolumeTemplateMsg apiMsg; public CreateDataVolumeFromVolumeTemplateMsg() { @@ -97,4 +98,12 @@ public APICreateDataVolumeFromVolumeTemplateMsg getApiMsg() { public void setApiMsg(APICreateDataVolumeFromVolumeTemplateMsg amsg) { this.apiMsg = amsg; } + + public Boolean getEncrypted() { + return encrypted; + } + + public void setEncrypted(Boolean encrypted) { + this.encrypted = encrypted; + } } diff --git a/header/src/main/java/org/zstack/header/volume/CreateVolumeMsg.java b/header/src/main/java/org/zstack/header/volume/CreateVolumeMsg.java index e8d22cb9a99..e684bfabdfd 100755 --- a/header/src/main/java/org/zstack/header/volume/CreateVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/volume/CreateVolumeMsg.java @@ -16,6 +16,7 @@ public class CreateVolumeMsg extends NeedReplyMessage implements VolumeCreateMes private String format; private String resourceUuid; private String protocol; + private Boolean encrypted; public String getFormat() { return format; @@ -130,4 +131,14 @@ public String getProtocol() { public void setProtocol(String protocol) { this.protocol = protocol; } + + @Override + public Boolean getEncrypted() { + return encrypted; + } + + @Override + public void setEncrypted(Boolean encrypted) { + this.encrypted = encrypted; + } } diff --git a/header/src/main/java/org/zstack/header/volume/EncryptVolumeMsg.java b/header/src/main/java/org/zstack/header/volume/EncryptVolumeMsg.java new file mode 100644 index 00000000000..3249dd21d65 --- /dev/null +++ b/header/src/main/java/org/zstack/header/volume/EncryptVolumeMsg.java @@ -0,0 +1,101 @@ +package org.zstack.header.volume; + +import org.zstack.header.message.ConfigurableTimeoutMessage; +import org.zstack.header.message.DefaultTimeout; +import org.zstack.header.message.NeedReplyMessage; + +import java.util.concurrent.TimeUnit; + +/** + * Convert an existing data volume's bits to a LUKS-encrypted form in place. + *

+ * Steps performed by the handler (in {@link org.zstack.storage.volume.VolumeBase}): + *

    + *
  1. Ensure the volume has a key-provider binding (auto-attaches the default key provider + * when none is bound yet).
  2. + *
  3. Materialize a DEK via the {@code EncryptedResourceKeyManager}.
  4. + *
  5. Stage the LUKS secret material file on the host + * ({@code SecretHostEnsureLuksSecretFileMsg}).
  6. + *
  7. Ask the primary storage backend to LUKS-convert the bits in place + * ({@code EncryptVolumeBitsOnPrimaryStorageMsg}).
  8. + *
  9. Persist {@code VolumeVO.encrypted = true}.
  10. + *
+ *

+ * If the volume is already encrypted (i.e. {@code VolumeVO.encrypted == true}) and the + * caller is the regular VM-attach path that hasn't preset the row, the handler treats it + * as a no-op success. Callers that have just allocated/downloaded fresh plain bits but + * pre-marked the row encrypted (the create-data-volume-from-template flow) MUST set + * {@link #force} to {@code true} so the encrypt step still runs. + */ +@DefaultTimeout(timeunit = TimeUnit.HOURS, value = 1) +public class EncryptVolumeMsg extends NeedReplyMessage implements VolumeMessage, ConfigurableTimeoutMessage { + private String volumeUuid; + private String hostUuid; + /** + * Optional. When null, the handler resolves it from {@code VolumeVO.primaryStorageUuid}. + */ + private String primaryStorageUuid; + /** + * Optional. When null, the handler resolves it from {@code VolumeVO.installPath}. + */ + private String installPath; + /** + * Free-form purpose label for the DEK get-or-create audit trail. + */ + private String purpose; + /** + * When true, the handler runs the encrypt step even if the volume row is already + * marked encrypted. Used by callers that pre-marked the row before downloading + * plain bits (e.g. CreateDataVolumeFromVolumeTemplateMsg flow). + */ + private boolean force; + + @Override + public String getVolumeUuid() { + return volumeUuid; + } + + public void setVolumeUuid(String volumeUuid) { + this.volumeUuid = volumeUuid; + } + + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + public String getInstallPath() { + return installPath; + } + + public void setInstallPath(String installPath) { + this.installPath = installPath; + } + + public String getPurpose() { + return purpose; + } + + public void setPurpose(String purpose) { + this.purpose = purpose; + } + + public boolean isForce() { + return force; + } + + public void setForce(boolean force) { + this.force = force; + } +} diff --git a/header/src/main/java/org/zstack/header/volume/EncryptVolumeReply.java b/header/src/main/java/org/zstack/header/volume/EncryptVolumeReply.java new file mode 100644 index 00000000000..f4f21f1b901 --- /dev/null +++ b/header/src/main/java/org/zstack/header/volume/EncryptVolumeReply.java @@ -0,0 +1,18 @@ +package org.zstack.header.volume; + +import org.zstack.header.message.MessageReply; + +/** + * Reply for {@link EncryptVolumeMsg}. + */ +public class EncryptVolumeReply extends MessageReply { + private VolumeInventory inventory; + + public VolumeInventory getInventory() { + return inventory; + } + + public void setInventory(VolumeInventory inventory) { + this.inventory = inventory; + } +} diff --git a/header/src/main/java/org/zstack/header/volume/InstantiateVolumeMsg.java b/header/src/main/java/org/zstack/header/volume/InstantiateVolumeMsg.java index 13e6f557b24..e454fecdd92 100755 --- a/header/src/main/java/org/zstack/header/volume/InstantiateVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/volume/InstantiateVolumeMsg.java @@ -12,6 +12,7 @@ public class InstantiateVolumeMsg extends NeedReplyMessage implements VolumeMess private boolean primaryStorageAllocated; private boolean skipIfExisting; private String allocatedInstallUrl; + private VolumeLuksAgentSpec volumeLuksAgentSpec; public String getAllocatedInstallUrl() { return allocatedInstallUrl; @@ -61,4 +62,12 @@ public boolean isSkipIfExisting() { public void setSkipIfExisting(boolean skipIfExisting) { this.skipIfExisting = skipIfExisting; } + + public VolumeLuksAgentSpec getVolumeLuksAgentSpec() { + return volumeLuksAgentSpec; + } + + public void setVolumeLuksAgentSpec(VolumeLuksAgentSpec volumeLuksAgentSpec) { + this.volumeLuksAgentSpec = volumeLuksAgentSpec; + } } diff --git a/header/src/main/java/org/zstack/header/volume/VolumeAO.java b/header/src/main/java/org/zstack/header/volume/VolumeAO.java index 3ddcdbccad3..203b30134a5 100755 --- a/header/src/main/java/org/zstack/header/volume/VolumeAO.java +++ b/header/src/main/java/org/zstack/header/volume/VolumeAO.java @@ -86,6 +86,9 @@ public class VolumeAO extends ResourceVO implements ShadowEntity { @Column private String protocol; + @Column + private boolean encrypted; + @Transient private VolumeAO shadow; @@ -298,4 +301,12 @@ public String getProtocol() { public void setProtocol(String protocol) { this.protocol = protocol; } + + public boolean isEncrypted() { + return encrypted; + } + + public void setEncrypted(boolean encrypted) { + this.encrypted = encrypted; + } } diff --git a/header/src/main/java/org/zstack/header/volume/VolumeAO_.java b/header/src/main/java/org/zstack/header/volume/VolumeAO_.java index 729d50eefbf..af03c918ca2 100755 --- a/header/src/main/java/org/zstack/header/volume/VolumeAO_.java +++ b/header/src/main/java/org/zstack/header/volume/VolumeAO_.java @@ -31,4 +31,5 @@ public class VolumeAO_ extends ResourceVO_ { public static volatile SingularAttribute isShareable; public static volatile SingularAttribute volumeQos; public static volatile SingularAttribute protocol; + public static volatile SingularAttribute encrypted; } diff --git a/header/src/main/java/org/zstack/header/volume/VolumeCreateMessage.java b/header/src/main/java/org/zstack/header/volume/VolumeCreateMessage.java index a93217a6166..36f8b47f870 100644 --- a/header/src/main/java/org/zstack/header/volume/VolumeCreateMessage.java +++ b/header/src/main/java/org/zstack/header/volume/VolumeCreateMessage.java @@ -20,4 +20,11 @@ public interface VolumeCreateMessage { void setSystemTags(List systemTags); void addSystemTag(String tag); + + default Boolean getEncrypted() { + return null; + } + + default void setEncrypted(Boolean encrypted) { + } } diff --git a/header/src/main/java/org/zstack/header/volume/VolumeInventory.java b/header/src/main/java/org/zstack/header/volume/VolumeInventory.java index 96d2ae67a62..f04fe219fed 100755 --- a/header/src/main/java/org/zstack/header/volume/VolumeInventory.java +++ b/header/src/main/java/org/zstack/header/volume/VolumeInventory.java @@ -156,6 +156,8 @@ public class VolumeInventory implements Serializable { private Timestamp lastAttachDate; private String protocol; + private Boolean encrypted; + public VolumeInventory() { } @@ -183,6 +185,7 @@ public VolumeInventory(VolumeInventory other) { this.lastVmInstanceUuid = other.lastVmInstanceUuid; this.lastAttachDate = other.lastAttachDate; this.protocol = other.protocol; + this.encrypted = other.encrypted; } @@ -213,6 +216,7 @@ public VolumeInventory(VolumeInventory other) { inv.setLastVmInstanceUuid(vo.getLastVmInstanceUuid()); inv.setLastAttachDate(vo.getLastAttachDate()); inv.setProtocol(vo.getProtocol()); + inv.setEncrypted(vo.isEncrypted()); return inv; } @@ -437,4 +441,12 @@ public String getProtocol() { public void setProtocol(String protocol) { this.protocol = protocol; } + + public Boolean getEncrypted() { + return encrypted; + } + + public void setEncrypted(Boolean encrypted) { + this.encrypted = encrypted; + } } diff --git a/header/src/main/java/org/zstack/header/volume/VolumeLuksAgentSpec.java b/header/src/main/java/org/zstack/header/volume/VolumeLuksAgentSpec.java new file mode 100644 index 00000000000..8602adcd79d --- /dev/null +++ b/header/src/main/java/org/zstack/header/volume/VolumeLuksAgentSpec.java @@ -0,0 +1,31 @@ +package org.zstack.header.volume; + +import java.io.Serializable; + +public class VolumeLuksAgentSpec implements Serializable { + private static final long serialVersionUID = 1L; + + private String encryptSecretUuid; + private String encryptLuksSecretMaterialFilePath; + + public boolean isComplete() { + return (encryptSecretUuid != null && !encryptSecretUuid.isEmpty()) + || (encryptLuksSecretMaterialFilePath != null && !encryptLuksSecretMaterialFilePath.isEmpty()); + } + + public String getEncryptSecretUuid() { + return encryptSecretUuid; + } + + public void setEncryptSecretUuid(String encryptSecretUuid) { + this.encryptSecretUuid = encryptSecretUuid; + } + + public String getEncryptLuksSecretMaterialFilePath() { + return encryptLuksSecretMaterialFilePath; + } + + public void setEncryptLuksSecretMaterialFilePath(String encryptLuksSecretMaterialFilePath) { + this.encryptLuksSecretMaterialFilePath = encryptLuksSecretMaterialFilePath; + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java index cc7c9083a7a..07014550b3d 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -484,6 +484,30 @@ public void setSecretUuid(String secretUuid) { } } + public static class SecretHostEnsureLuksSecretFileCmd extends AgentCommand { + private String encryptedDek; + + public String getEncryptedDek() { + return encryptedDek; + } + + public void setEncryptedDek(String encryptedDek) { + this.encryptedDek = encryptedDek; + } + } + + public static class SecretHostEnsureLuksSecretFileResponse extends AgentResponse { + private String secFilePath; + + public String getSecFilePath() { + return secFilePath; + } + + public void setSecFilePath(String secFilePath) { + this.secFilePath = secFilePath; + } + } + public static class SecretHostGetCmd extends AgentCommand { private String vmUuid; private String purpose; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java index 35ffa2c2065..ca3e7593184 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -133,6 +133,7 @@ public interface KVMConstant { String KVM_VERIFY_ENVELOPE_KEY_PATH = "/host/key/envelope/checkEnvelopeKey"; String KVM_GET_SECRET_PATH = "/host/key/envelope/getSecret"; String KVM_ENSURE_SECRET_PATH = "/host/key/envelope/ensureSecret"; + String KVM_WRITE_SECRET_MATERIAL_FILE_PATH = "/host/key/envelope/writeSecretMaterialFile"; String KVM_DELETE_SECRET_PATH = "/host/key/envelope/deleteSecret"; /** HTTP timeout in seconds for envelope key sync (verify/create/rotate/get) to agent. */ @@ -141,6 +142,7 @@ public interface KVMConstant { /** Max size in bytes for DEK payload in SecretHostDefine (decoded from dekBase64). */ int MAX_DEK_BYTES = 1024; String HOST_SECRET_USAGE_INSTANCE_VTPM = "tpm0"; + String HOST_SECRET_USAGE_INSTANCE_VOLUME = "volume"; String KVM_HOST_FILE_DOWNLOAD_PATH = "/host/file/download"; String KVM_HOST_FILE_UPLOAD_PATH = "/host/file/upload"; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index 828aeb3a7cd..8a33f175fd1 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -57,6 +57,8 @@ import org.zstack.header.host.MigrateVmOnHypervisorMsg.StorageMigrationPolicy; import org.zstack.header.secret.SecretHostDefineMsg; import org.zstack.header.secret.SecretHostDefineReply; +import org.zstack.header.secret.SecretHostEnsureLuksSecretFileMsg; +import org.zstack.header.secret.SecretHostEnsureLuksSecretFileReply; import org.zstack.header.secret.SecretHostDeleteMsg; import org.zstack.header.secret.SecretHostDeleteReply; import org.zstack.header.secret.SecretHostGetMsg; @@ -760,6 +762,8 @@ protected void handleLocalMessage(Message msg) { handle((SecretHostGetMsg) msg); } else if (msg instanceof ResolveVtpmLibvirtSecretOnHypervisorMsg) { handle((ResolveVtpmLibvirtSecretOnHypervisorMsg) msg); + } else if (msg instanceof SecretHostEnsureLuksSecretFileMsg) { + handle((SecretHostEnsureLuksSecretFileMsg) msg); } else if (msg instanceof SecretHostDefineMsg) { handle((SecretHostDefineMsg) msg); } else if (msg instanceof SecretHostDeleteMsg) { @@ -5408,6 +5412,116 @@ public void run(MessageReply r) { }); } + private void handle(SecretHostEnsureLuksSecretFileMsg msg) { + SecretHostEnsureLuksSecretFileReply reply = new SecretHostEnsureLuksSecretFileReply(); + if (org.apache.commons.lang.StringUtils.isBlank(msg.getDekBase64())) { + reply.setError(operr("dekBase64 is required")); + bus.reply(msg, reply); + return; + } + if (StringUtils.isBlank(msg.getHostUuid())) { + reply.setError(operr("hostUuid is required for LUKS secret material file on hypervisor")); + bus.reply(msg, reply); + return; + } + String hostUuid = getSelf().getUuid(); + HostKeyIdentityVO identity = HostKeyIdentityHelper.getHostKeyIdentity(dbf, hostUuid); + String pubKey = identity != null ? org.apache.commons.lang.StringUtils.trimToNull(identity.getPublicKey()) : null; + Boolean verifyOk = identity != null ? identity.getVerified() : null; + if (pubKey == null) { + reply.setError(operr("no public key for host, connect/reconnect did not sync key")); + bus.reply(msg, reply); + return; + } + String storedFingerprint = StringUtils.trimToNull(identity.getFingerprint()); + String computed = HostKeyIdentityHelper.fingerprintFromPublicKey(pubKey); + if (storedFingerprint == null || !StringUtils.equals(storedFingerprint, computed)) { + reply.setError(operr("host public key fingerprint mismatch, key may be corrupted or tampered")); + bus.reply(msg, reply); + return; + } + if (!Boolean.TRUE.equals(verifyOk)) { + reply.setError(operr("host secret key verify not ok, not synced")); + bus.reply(msg, reply); + return; + } + byte[] dekRaw; + try { + dekRaw = java.util.Base64.getDecoder().decode(msg.getDekBase64().trim()); + } catch (IllegalArgumentException e) { + reply.setError(operr("invalid dekBase64: %s", e.getMessage())); + bus.reply(msg, reply); + return; + } + if (dekRaw == null || dekRaw.length == 0) { + reply.setError(operr("dekBase64 decoded to empty")); + bus.reply(msg, reply); + return; + } + if (dekRaw.length > KVMConstant.MAX_DEK_BYTES) { + reply.setError(operr("dekBase64 decoded payload is too large")); + bus.reply(msg, reply); + return; + } + byte[] pubKeyBytes; + try { + pubKeyBytes = java.util.Base64.getDecoder().decode(pubKey); + } catch (IllegalArgumentException e) { + reply.setError(operr("invalid host public key in DB: %s", e.getMessage())); + bus.reply(msg, reply); + return; + } + if (pubKeyBytes == null || pubKeyBytes.length != 32) { + reply.setError(operr("host public key must be 32 bytes (X25519)")); + bus.reply(msg, reply); + return; + } + java.util.List sealers = pluginRegistry.getExtensionList(HostSecretEnvelopeCryptoExtensionPoint.class); + if (sealers == null || sealers.isEmpty()) { + reply.setError(operr("host secret envelope sealer not available (premium crypto module required)")); + bus.reply(msg, reply); + return; + } + byte[] envelope; + try { + envelope = sealers.get(0).seal(pubKeyBytes, dekRaw); + } catch (Exception e) { + reply.setError(operr("HPKE seal failed: %s", e.getMessage())); + bus.reply(msg, reply); + return; + } + String envelopeDekBase64 = java.util.Base64.getEncoder().encodeToString(envelope); + KVMAgentCommands.SecretHostEnsureLuksSecretFileCmd cmd = new KVMAgentCommands.SecretHostEnsureLuksSecretFileCmd(); + cmd.setEncryptedDek(envelopeDekBase64); + + KVMHostAsyncHttpCallMsg kmsg = new KVMHostAsyncHttpCallMsg(); + kmsg.setCommand(cmd); + kmsg.setPath(KVMConstant.KVM_WRITE_SECRET_MATERIAL_FILE_PATH); + kmsg.setHostUuid(msg.getHostUuid()); + bus.makeTargetServiceIdByResourceUuid(kmsg, HostConstant.SERVICE_ID, msg.getHostUuid()); + bus.send(kmsg, new CloudBusCallBack(msg) { + @Override + public void run(MessageReply r) { + if (!r.isSuccess()) { + reply.setError(r.getError()); + bus.reply(msg, reply); + return; + } + KVMHostAsyncHttpCallReply kreply = r.castReply(); + KVMAgentCommands.SecretHostEnsureLuksSecretFileResponse rsp = + kreply.toResponse(KVMAgentCommands.SecretHostEnsureLuksSecretFileResponse.class); + if (rsp != null && rsp.isSuccess() && StringUtils.isNotBlank(rsp.getSecFilePath())) { + reply.setSecFilePath(rsp.getSecFilePath()); + } else if (rsp != null && rsp.isSuccess()) { + reply.setError(operr("prepare LUKS secret channel succeeded but secFilePath is empty")); + } else { + reply.setError(buildSecretAgentError(rsp, "prepare LUKS secret channel failed")); + } + bus.reply(msg, reply); + } + }); + } + private void handle(SecretHostDefineMsg msg) { SecretHostDefineReply reply = new SecretHostDefineReply(); if (org.apache.commons.lang.StringUtils.isBlank(msg.getDekBase64())) { diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java index eea5f8beb92..e4fb64dbfd0 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java @@ -907,11 +907,30 @@ public void handleLocalMessage(Message msg) { handle((CommitVolumeSnapshotOnPrimaryStorageMsg) msg); } else if (msg instanceof PullVolumeSnapshotOnPrimaryStorageMsg) { handle((PullVolumeSnapshotOnPrimaryStorageMsg) msg); + } else if (msg instanceof EncryptVolumeBitsOnPrimaryStorageMsg) { + handle((EncryptVolumeBitsOnPrimaryStorageMsg) msg); } else { super.handleLocalMessage(msg); } } + private void handle(EncryptVolumeBitsOnPrimaryStorageMsg msg) { + LocalStorageHypervisorBackend bkd = getHypervisorBackendFactoryByHostUuid(msg.getHostUuid()).getHypervisorBackend(self); + bkd.handle(msg, new ReturnValueCompletion(msg) { + @Override + public void success(EncryptVolumeBitsOnPrimaryStorageReply reply) { + bus.reply(msg, reply); + } + + @Override + public void fail(ErrorCode errorCode) { + EncryptVolumeBitsOnPrimaryStorageReply reply = new EncryptVolumeBitsOnPrimaryStorageReply(); + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } + private void handle(DownloadBitsFromKVMHostToPrimaryStorageMsg msg) { LocalStorageHypervisorBackend bkd = getHypervisorBackendFactoryByHostUuid(msg.getSrcHostUuid()).getHypervisorBackend(self); bkd.handle(msg, new ReturnValueCompletion(msg) { diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageCreateEmptyVolumeMsg.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageCreateEmptyVolumeMsg.java index 414b564bc14..c882af3ff45 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageCreateEmptyVolumeMsg.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageCreateEmptyVolumeMsg.java @@ -3,6 +3,7 @@ import org.zstack.header.message.NeedReplyMessage; import org.zstack.header.storage.primary.PrimaryStorageMessage; import org.zstack.header.volume.VolumeInventory; +import org.zstack.header.volume.VolumeLuksAgentSpec; /** * Created by frank on 10/24/2015. @@ -12,6 +13,7 @@ public class LocalStorageCreateEmptyVolumeMsg extends NeedReplyMessage implement private String hostUuid; private String backingFile; private VolumeInventory volume; + private VolumeLuksAgentSpec volumeLuksAgentSpec; public String getBackingFile() { return backingFile; @@ -45,4 +47,12 @@ public VolumeInventory getVolume() { public void setVolume(VolumeInventory volume) { this.volume = volume; } + + public VolumeLuksAgentSpec getVolumeLuksAgentSpec() { + return volumeLuksAgentSpec; + } + + public void setVolumeLuksAgentSpec(VolumeLuksAgentSpec volumeLuksAgentSpec) { + this.volumeLuksAgentSpec = volumeLuksAgentSpec; + } } diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageHypervisorBackend.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageHypervisorBackend.java index d8226d932b2..f0ec6ed22dd 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageHypervisorBackend.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageHypervisorBackend.java @@ -108,9 +108,11 @@ public LocalStorageHypervisorBackend(PrimaryStorageVO self) { abstract void deleteBits(String path, String hostUuid, Completion completion); - abstract void createEmptyVolume(VolumeInventory volume, String hostUuid, ReturnValueCompletion completion); + abstract void createEmptyVolume(VolumeInventory volume, String hostUuid, VolumeLuksAgentSpec volumeLuksAgentSpec, + ReturnValueCompletion completion); - abstract void createEmptyVolumeWithBackingFile(VolumeInventory volume, String hostUuid, String backingFile, ReturnValueCompletion completion); + abstract void createEmptyVolumeWithBackingFile(VolumeInventory volume, String hostUuid, String backingFile, + VolumeLuksAgentSpec volumeLuksAgentSpec, ReturnValueCompletion completion); abstract void checkHostAttachedPSMountPath(String hostUuid, ReturnValueCompletion completion); @@ -133,4 +135,6 @@ public LocalStorageHypervisorBackend(PrimaryStorageVO self) { abstract void handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion); abstract void handle(RebaseVolumeBackingFileOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion); + + abstract void handle(EncryptVolumeBitsOnPrimaryStorageMsg msg, ReturnValueCompletion completion); } diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java index 5b4324495b2..c674ada969a 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java @@ -205,6 +205,7 @@ public static class CreateEmptyVolumeCmd extends AgentCommand { private String volumeUuid; private String backingFile; private String volumeFormat; + private String encryptLuksSecretMaterialFilePath; public String getBackingFile() { return backingFile; @@ -222,6 +223,14 @@ public void setVolumeFormat(String volumeFormat) { this.volumeFormat = volumeFormat; } + public String getEncryptLuksSecretMaterialFilePath() { + return encryptLuksSecretMaterialFilePath; + } + + public void setEncryptLuksSecretMaterialFilePath(String encryptLuksSecretMaterialFilePath) { + this.encryptLuksSecretMaterialFilePath = encryptLuksSecretMaterialFilePath; + } + public String getInstallUrl() { return installUrl; } @@ -268,6 +277,14 @@ public static class CreateEmptyVolumeRsp extends AgentResponse { public Long size; } + public static class EncryptVolumeBitsCmd extends AgentCommand { + public String installPath; + public String encryptLuksSecretMaterialFilePath; + } + + public static class EncryptVolumeBitsRsp extends AgentResponse { + } + public static class GetPhysicalCapacityCmd extends AgentCommand { private String hostUuid; @@ -285,6 +302,15 @@ public static class CreateVolumeFromCacheCmd extends AgentCommand { private String installUrl; private String volumeUuid; private long virtualSize; + private String encryptLuksSecretMaterialFilePath; + + public String getEncryptLuksSecretMaterialFilePath() { + return encryptLuksSecretMaterialFilePath; + } + + public void setEncryptLuksSecretMaterialFilePath(String encryptLuksSecretMaterialFilePath) { + this.encryptLuksSecretMaterialFilePath = encryptLuksSecretMaterialFilePath; + } public String getTemplatePathInCache() { return templatePathInCache; @@ -997,6 +1023,7 @@ public static class PrefixRebaseBackingFilesRsp extends LocalStorageKvmBackend.A public static final String SCAN_VM_METADATA_PATH = "/localstorage/vm/metadata/scan"; public static final String CLEANUP_VM_METADATA_PATH = "/localstorage/vm/metadata/cleanup"; public static final String PREFIX_REBASE_BACKING_FILES_PATH = "/localstorage/snapshot/prefixrebasebackingfiles"; + public static final String ENCRYPT_VOLUME_BITS_PATH = "/localstorage/volume/encryptinplace"; public LocalStorageKvmBackend() { } @@ -1309,7 +1336,7 @@ private void createTemporaryEmptyVolume(InstantiateTemporaryVolumeOnPrimaryStora } private void createEmptyVolume(InstantiateVolumeOnPrimaryStorageMsg msg, ReturnValueCompletion completion) { - createEmptyVolume(msg.getVolume(), msg.getDestHost().getUuid(), new ReturnValueCompletion(completion) { + createEmptyVolume(msg.getVolume(), msg.getDestHost().getUuid(), msg.getVolumeLuksAgentSpec(), new ReturnValueCompletion(completion) { @Override public void success(VolumeStats returnValue) { InstantiateVolumeOnPrimaryStorageReply r = new InstantiateVolumeOnPrimaryStorageReply(); @@ -1332,11 +1359,11 @@ public void fail(ErrorCode errorCode) { }); } - public void createEmptyVolume(final VolumeInventory volume, final String hostUuid, final ReturnValueCompletion completion) { - createEmptyVolumeWithBackingFile(volume, hostUuid, null, completion); + public void createEmptyVolume(final VolumeInventory volume, final String hostUuid, final VolumeLuksAgentSpec volumeLuksAgentSpec, final ReturnValueCompletion completion) { + createEmptyVolumeWithBackingFile(volume, hostUuid, null, volumeLuksAgentSpec, completion); } - public void createEmptyVolumeWithBackingFile(final VolumeInventory volume, final String hostUuid, final String backingFile, final ReturnValueCompletion completion) { + public void createEmptyVolumeWithBackingFile(final VolumeInventory volume, final String hostUuid, final String backingFile, final VolumeLuksAgentSpec volumeLuksAgentSpec, final ReturnValueCompletion completion) { final CreateEmptyVolumeCmd cmd = new CreateEmptyVolumeCmd(); cmd.setAccountUuid(acntMgr.getOwnerAccountUuidOfResource(volume.getUuid())); if (volume.getInstallPath() != null && !volume.getInstallPath().equals("")) { @@ -1358,6 +1385,9 @@ public void createEmptyVolumeWithBackingFile(final VolumeInventory volume, final cmd.setSize(volume.getSize()); cmd.setVolumeUuid(volume.getUuid()); cmd.setBackingFile(backingFile); + if (volumeLuksAgentSpec != null && volumeLuksAgentSpec.isComplete()) { + cmd.setEncryptLuksSecretMaterialFilePath(volumeLuksAgentSpec.getEncryptLuksSecretMaterialFilePath()); + } httpCall(CREATE_EMPTY_VOLUME_PATH, hostUuid, cmd, CreateEmptyVolumeRsp.class, new ReturnValueCompletion(completion) { @Override @@ -1707,7 +1737,7 @@ private void createRootVolume(final InstantiateRootVolumeFromTemplateOnPrimarySt final ImageInventory image = ispec.getInventory(); if (!ImageMediaType.RootVolumeTemplate.toString().equals(image.getMediaType())) { - createEmptyVolume(msg.getVolume(), msg.getDestHost().getUuid(), new ReturnValueCompletion(completion) { + createEmptyVolume(msg.getVolume(), msg.getDestHost().getUuid(), msg.getVolumeLuksAgentSpec(), new ReturnValueCompletion(completion) { @Override public void success(VolumeStats returnValue) { InstantiateVolumeOnPrimaryStorageReply r = new InstantiateVolumeOnPrimaryStorageReply(); @@ -1778,6 +1808,11 @@ public void run(final FlowTrigger trigger, Map data) { cmd.setVirtualSize(volume.getSize()); } + VolumeLuksAgentSpec volumeLuksAgentSpec = msg.getVolumeLuksAgentSpec(); + if (volumeLuksAgentSpec != null && volumeLuksAgentSpec.isComplete()) { + cmd.setEncryptLuksSecretMaterialFilePath(volumeLuksAgentSpec.getEncryptLuksSecretMaterialFilePath()); + } + httpCall(CREATE_VOLUME_FROM_CACHE_PATH, hostUuid, cmd, CreateVolumeFromCacheRsp.class, new ReturnValueCompletion(trigger) { @Override public void success(CreateVolumeFromCacheRsp returnValue) { @@ -2359,7 +2394,7 @@ public void run(MessageReply reply) { @Override void handle(LocalStorageCreateEmptyVolumeMsg msg, final ReturnValueCompletion completion) { - createEmptyVolumeWithBackingFile(msg.getVolume(), msg.getHostUuid(), msg.getBackingFile(), new ReturnValueCompletion(completion) { + createEmptyVolumeWithBackingFile(msg.getVolume(), msg.getHostUuid(), msg.getBackingFile(), msg.getVolumeLuksAgentSpec(), new ReturnValueCompletion(completion) { @Override public void success(VolumeStats returnValue) { LocalStorageCreateEmptyVolumeReply reply = new LocalStorageCreateEmptyVolumeReply(); @@ -3976,4 +4011,24 @@ public void fail(ErrorCode errorCode) { } }); } + + @Override + void handle(EncryptVolumeBitsOnPrimaryStorageMsg msg, ReturnValueCompletion completion) { + EncryptVolumeBitsCmd cmd = new EncryptVolumeBitsCmd(); + cmd.installPath = msg.getInstallPath(); + cmd.encryptLuksSecretMaterialFilePath = msg.getEncryptLuksSecretMaterialFilePath(); + + httpCall(ENCRYPT_VOLUME_BITS_PATH, msg.getHostUuid(), cmd, EncryptVolumeBitsRsp.class, new ReturnValueCompletion(completion) { + @Override + public void success(EncryptVolumeBitsRsp rsp) { + completion.success(new EncryptVolumeBitsOnPrimaryStorageReply()); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(operr("failed to encrypt volume[uuid:%s] bits at path[%s] on host[uuid:%s]: %s", + msg.getVolumeUuid(), msg.getInstallPath(), msg.getHostUuid(), errorCode)); + } + }); + } } diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmFactory.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmFactory.java index 8549d799eeb..337b57715fb 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmFactory.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmFactory.java @@ -195,7 +195,7 @@ public void beforeTakeSnapshot(KVMHostInventory host, TakeSnapshotOnHypervisorMs LocalStorageHypervisorBackend bkd = getHypervisorBackend(primaryStorageVO); String backingFile = cmd.isOnline() ? cmd.getVolumeInstallPath() : null; - bkd.createEmptyVolumeWithBackingFile(inv, msg.getHostUuid(), backingFile, new ReturnValueCompletion(completion) { + bkd.createEmptyVolumeWithBackingFile(inv, msg.getHostUuid(), backingFile, null, new ReturnValueCompletion(completion) { @Override public void success(VolumeStats returnValue) { completion.success(); diff --git a/storage/pom.xml b/storage/pom.xml index c56a401e16c..f58a442e2e5 100755 --- a/storage/pom.xml +++ b/storage/pom.xml @@ -54,6 +54,11 @@ longjob ${project.version} + + org.zstack + kvm + ${project.version} + diff --git a/storage/src/main/java/org/zstack/storage/encrypt/DummyVolumeEncryptedResourceKeyBackend.java b/storage/src/main/java/org/zstack/storage/encrypt/DummyVolumeEncryptedResourceKeyBackend.java new file mode 100644 index 00000000000..f57fffd399b --- /dev/null +++ b/storage/src/main/java/org/zstack/storage/encrypt/DummyVolumeEncryptedResourceKeyBackend.java @@ -0,0 +1,38 @@ +package org.zstack.storage.encrypt; + +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +/** + * OSS / no-premium-crypto: no-op volume key-provider persistence, same role as + * {@link org.zstack.compute.vm.devices.DummyTpmEncryptedResourceKeyBackend}. + */ +public class DummyVolumeEncryptedResourceKeyBackend implements VolumeEncryptedResourceKeyBackend { + private static final CLogger logger = Utils.getLogger(DummyVolumeEncryptedResourceKeyBackend.class); + + @Override + public void attachKeyProviderToVolume(String volumeUuid, String keyProviderUuid) { + logger.debug(String.format("ignore attach key provider to volume[uuid:%s] keyProviderUuid:%s", + volumeUuid, keyProviderUuid)); + } + + @Override + public void detachKeyProviderFromVolume(String volumeUuid) { + logger.debug(String.format("ignore detach key provider from volume[uuid:%s]", volumeUuid)); + } + + @Override + public String findKeyProviderUuidByVolume(String volumeUuid) { + return null; + } + + @Override + public boolean checkVolumeKeyProviderAttached(String volumeUuid) { + return false; + } + + @Override + public String defaultKeyProviderUuid() { + return null; + } +} diff --git a/storage/src/main/java/org/zstack/storage/encrypt/VolumeEncryptedInitialExtension.java b/storage/src/main/java/org/zstack/storage/encrypt/VolumeEncryptedInitialExtension.java new file mode 100644 index 00000000000..44cc444a6fc --- /dev/null +++ b/storage/src/main/java/org/zstack/storage/encrypt/VolumeEncryptedInitialExtension.java @@ -0,0 +1,241 @@ +package org.zstack.storage.encrypt; + +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Configurable; +import org.zstack.core.cloudbus.CloudBus; +import org.zstack.core.cloudbus.CloudBusCallBack; +import org.zstack.core.db.DatabaseFacade; +import org.zstack.core.db.Q; +import org.zstack.header.core.FutureReturnValueCompletion; +import org.zstack.header.errorcode.OperationFailureException; +import org.zstack.header.host.HostConstant; +import org.zstack.header.host.HostInventory; +import org.zstack.header.keyprovider.EncryptedResourceKeyManager; +import org.zstack.header.message.MessageReply; +import org.zstack.header.secret.SecretHostDefineMsg; +import org.zstack.header.secret.SecretHostDefineReply; +import org.zstack.header.secret.SecretHostEnsureLuksSecretFileMsg; +import org.zstack.header.secret.SecretHostEnsureLuksSecretFileReply; +import org.zstack.header.storage.primary.InstantiateVolumeOnPrimaryStorageMsg; +import org.zstack.header.volume.AfterInstantiateVolumeExtensionPoint; +import org.zstack.header.volume.InstantiateVolumeMsg; +import org.zstack.header.volume.PreInstantiateVolumeExtensionPoint; +import org.zstack.header.volume.VolumeInventory; +import org.zstack.header.volume.VolumeLuksAgentSpec; +import org.zstack.header.volume.VolumeVO; +import org.zstack.header.volume.VolumeVO_; +import org.zstack.kvm.KVMConstant; + +import java.util.concurrent.TimeUnit; + +import static org.zstack.core.Platform.operr; + +/** + * Encrypted volume instantiate: {@link #preInstantiateVolume} prepares host LUKS secret material file; + * {@link #afterInstantiateVolume} defines the libvirt secret on the host (needs DEK again after async PS step). + */ +@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) +public class VolumeEncryptedInitialExtension implements PreInstantiateVolumeExtensionPoint, AfterInstantiateVolumeExtensionPoint { + + @Autowired + private DatabaseFacade dbf; + @Autowired + private CloudBus bus; + @Autowired(required = false) + private EncryptedResourceKeyManager encryptedResourceKeyManager; + @Autowired + private VolumeEncryptedResourceKeyBackend volumeEncryptedResourceKeyBackend; + + @Override + public void preInstantiateVolume(InstantiateVolumeMsg msg) { + if (encryptedResourceKeyManager == null) { + return; + } + String hostUuid = msg.getHostUuid(); + if (StringUtils.isBlank(hostUuid)) { + return; + } + + VolumeLuksAgentSpec spec = new VolumeLuksAgentSpec(); + msg.setVolumeLuksAgentSpec(spec); + + String volUuid = msg.getVolumeUuid(); + VolumeVO volume = dbf.findByUuid(volUuid, VolumeVO.class); + + if (volume != null && volume.isEncrypted()) { + // Auto-attach the default key provider when this volume has not yet been bound. + // No EncryptedResourceKeyRefVO row is written at create-time anymore (the previous + // CreateDataVolumeExtensionPoint.afterCreateVolume hook has been removed); binding + // is performed lazily here on first instantiate. + String kpUuid = volumeEncryptedResourceKeyBackend.findKeyProviderUuidByVolume(volUuid); + if (StringUtils.isBlank(kpUuid)) { + kpUuid = volumeEncryptedResourceKeyBackend.defaultKeyProviderUuid(); + if (StringUtils.isBlank(kpUuid)) { + throw new OperationFailureException(operr( + "encrypted volume[uuid:%s] has no key provider binding and no default key provider configured", + volUuid)); + } + volumeEncryptedResourceKeyBackend.attachKeyProviderToVolume(volUuid, kpUuid); + } + + EncryptedResourceKeyManager.ResourceKeyResult keyResult = materializeDek(volUuid, kpUuid); + String dekBase64 = keyResult.getDekBase64(); + if (StringUtils.isBlank(dekBase64)) { + throw new OperationFailureException(operr( + "encrypted volume[uuid:%s]: key manager returned empty DEK after materialization", + volUuid)); + } + + String secFilePath = ensureLuksSecretFileOnHost(hostUuid, volUuid, dekBase64); + spec.setEncryptLuksSecretMaterialFilePath(secFilePath); + } + } + + @Override + public void afterInstantiateVolume(InstantiateVolumeOnPrimaryStorageMsg msg) { + if (encryptedResourceKeyManager == null) { + return; + } + VolumeInventory volInv = msg.getVolume(); + if (volInv == null || !Boolean.TRUE.equals(volInv.getEncrypted())) { + return; + } + String volUuid = volInv.getUuid(); + VolumeLuksAgentSpec spec = msg.getVolumeLuksAgentSpec(); + if (spec == null || StringUtils.isBlank(spec.getEncryptLuksSecretMaterialFilePath())) { + return; + } + if (StringUtils.isNotBlank(spec.getEncryptSecretUuid())) { + return; + } + HostInventory destHost = msg.getDestHost(); + if (destHost == null || StringUtils.isBlank(destHost.getUuid())) { + return; + } + String hostUuid = destHost.getUuid(); + + VolumeVO volume = dbf.findByUuid(volUuid, VolumeVO.class); + if (volume == null || !volume.isEncrypted()) { + return; + } + String kpUuid = volumeEncryptedResourceKeyBackend.findKeyProviderUuidByVolume(volUuid); + if (StringUtils.isBlank(kpUuid)) { + throw new OperationFailureException(operr( + "encrypted volume[uuid:%s] has no key provider binding (EncryptedResourceKeyRefVO.providerUuid is empty or missing)", + volUuid)); + } + + EncryptedResourceKeyManager.ResourceKeyResult keyResult = materializeDek(volUuid, kpUuid); + String dekBase64 = keyResult.getDekBase64(); + if (StringUtils.isBlank(dekBase64)) { + throw new OperationFailureException(operr( + "encrypted volume[uuid:%s]: key manager returned empty DEK for libvirt secret", + volUuid)); + } + + String vmUuid = lookupVmInstanceUuid(volUuid); + String libvirtSecretUuid = defineLibvirtSecretOnHost(hostUuid, vmUuid, volUuid, dekBase64, keyResult.getKeyVersion()); + + spec.setEncryptSecretUuid(libvirtSecretUuid); + } + + private EncryptedResourceKeyManager.ResourceKeyResult materializeDek(String volUuid, String kpUuid) { + EncryptedResourceKeyManager.GetOrCreateResourceKeyContext ctx = + new EncryptedResourceKeyManager.GetOrCreateResourceKeyContext(); + ctx.setResourceUuid(volUuid); + ctx.setResourceType(VolumeVO.class.getSimpleName()); + ctx.setKeyProviderUuid(kpUuid); + ctx.setPurpose("instantiate-volume"); + + FutureReturnValueCompletion keyFuture = new FutureReturnValueCompletion(null); + encryptedResourceKeyManager.getOrCreateKey(ctx, keyFuture); + keyFuture.await(TimeUnit.MINUTES.toMillis(2)); + if (!keyFuture.isSuccess()) { + throw new OperationFailureException(operr( + "failed to materialize encryption key for volume[uuid:%s]: %s", + volUuid, keyFuture.getErrorCode())); + } + return keyFuture.getResult(); + } + + private String ensureLuksSecretFileOnHost(String hostUuid, String volUuid, String dekBase64) { + SecretHostEnsureLuksSecretFileMsg ensureMsg = new SecretHostEnsureLuksSecretFileMsg(); + ensureMsg.setHostUuid(hostUuid); + ensureMsg.setDekBase64(dekBase64); + bus.makeTargetServiceIdByResourceUuid(ensureMsg, HostConstant.SERVICE_ID, hostUuid); + + FutureReturnValueCompletion secretFuture = new FutureReturnValueCompletion(null); + bus.send(ensureMsg, new CloudBusCallBack(secretFuture) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + secretFuture.fail(reply.getError()); + return; + } + SecretHostEnsureLuksSecretFileReply r = reply.castReply(); + if (StringUtils.isBlank(r.getSecFilePath())) { + secretFuture.fail(operr( + "ensure LUKS secret file on host succeeded but secFilePath is empty, host[uuid:%s]", + hostUuid)); + return; + } + secretFuture.success(r.getSecFilePath()); + } + }); + secretFuture.await(TimeUnit.MINUTES.toMillis(2)); + if (!secretFuture.isSuccess()) { + throw new OperationFailureException(operr( + "failed to prepare LUKS secret material file for encrypted volume[uuid:%s] on host[uuid:%s]: %s", + volUuid, hostUuid, secretFuture.getErrorCode())); + } + return secretFuture.getResult(); + } + + private String defineLibvirtSecretOnHost(String hostUuid, String vmUuid, String volUuid, + String dekBase64, Integer keyVersion) { + SecretHostDefineMsg defineMsg = new SecretHostDefineMsg(); + defineMsg.setHostUuid(hostUuid); + defineMsg.setVmUuid(vmUuid); + defineMsg.setDekBase64(dekBase64); + defineMsg.setPurpose("volume"); + defineMsg.setKeyVersion(keyVersion); + defineMsg.setUsageInstance(KVMConstant.HOST_SECRET_USAGE_INSTANCE_VOLUME); + defineMsg.setDescription(String.format("LUKS DEK for volume %s", volUuid)); + bus.makeTargetServiceIdByResourceUuid(defineMsg, HostConstant.SERVICE_ID, hostUuid); + + FutureReturnValueCompletion defineFuture = new FutureReturnValueCompletion(null); + bus.send(defineMsg, new CloudBusCallBack(defineFuture) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + defineFuture.fail(reply.getError()); + return; + } + SecretHostDefineReply r = reply.castReply(); + if (StringUtils.isBlank(r.getSecretUuid())) { + defineFuture.fail(operr( + "ensure volume LUKS secret on host succeeded but secretUuid is empty, host[uuid:%s]", + hostUuid)); + return; + } + defineFuture.success(r.getSecretUuid()); + } + }); + defineFuture.await(TimeUnit.MINUTES.toMillis(2)); + if (!defineFuture.isSuccess()) { + throw new OperationFailureException(operr( + "failed to ensure libvirt secret for encrypted volume[uuid:%s] on host[uuid:%s]: %s", + volUuid, hostUuid, defineFuture.getErrorCode())); + } + return defineFuture.getResult(); + } + + private String lookupVmInstanceUuid(String volumeUuid) { + return Q.New(VolumeVO.class) + .eq(VolumeVO_.uuid, volumeUuid) + .select(VolumeVO_.vmInstanceUuid) + .findValue(); + } +} diff --git a/storage/src/main/java/org/zstack/storage/encrypt/VolumeEncryptedResourceKeyBackend.java b/storage/src/main/java/org/zstack/storage/encrypt/VolumeEncryptedResourceKeyBackend.java new file mode 100644 index 00000000000..e49df1024c6 --- /dev/null +++ b/storage/src/main/java/org/zstack/storage/encrypt/VolumeEncryptedResourceKeyBackend.java @@ -0,0 +1,34 @@ +package org.zstack.storage.encrypt; + +/** + * Handles {@link org.zstack.header.volume.VolumeVO} rows in {@link org.zstack.header.keyprovider.EncryptedResourceKeyRefVO} + * (key provider binding for LUKS volumes), analogous to {@link org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend} + * for TPM. + */ +public interface VolumeEncryptedResourceKeyBackend { + + /** + * Link a volume to a key provider (placeholder ref row). Non-async. + */ + void attachKeyProviderToVolume(String volumeUuid, String keyProviderUuid); + + /** + * Remove key-provider binding for the volume. Non-async. + */ + void detachKeyProviderFromVolume(String volumeUuid); + + /** + * @return provider uuid or null when not bound / crypto not installed + */ + String findKeyProviderUuidByVolume(String volumeUuid); + + /** + * Whether an {@code EncryptedResourceKeyRefVO} row exists for this volume. + */ + boolean checkVolumeKeyProviderAttached(String volumeUuid); + + /** + * Global default key provider uuid, or null (e.g. NONE / crypto not installed). + */ + String defaultKeyProviderUuid(); +} diff --git a/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java b/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java index f722f5b788a..f00cabb3464 100755 --- a/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java +++ b/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java @@ -1,5 +1,6 @@ package org.zstack.storage.volume; +import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Configurable; @@ -110,6 +111,8 @@ public class VolumeBase extends AbstractVolume implements Volume { private VmInstanceResourceMetadataManager vidm; @Autowired private StorageTrash trash; + @Autowired + private VolumeInPlaceEncryptor volumeInPlaceEncryptor; public VolumeBase(VolumeVO vo) { self = vo; @@ -177,6 +180,8 @@ private void handleLocalMessage(Message msg) { handle((FlattenVolumeMsg) msg); } else if (msg instanceof CancelFlattenVolumeMsg) { handle((CancelFlattenVolumeMsg) msg); + } else if (msg instanceof EncryptVolumeMsg) { + handle((EncryptVolumeMsg) msg); } else { bus.dealWithUnknownMessage(msg); } @@ -616,6 +621,7 @@ private void prepareMsg(InstantiateVolumeMsg msg, InstantiateVolumeOnPrimaryStor imsg.setSystemTags(msg.getSystemTags()); imsg.setSkipIfExisting(msg.isSkipIfExisting()); imsg.setAllocatedInstallUrl(msg.getAllocatedInstallUrl()); + imsg.setVolumeLuksAgentSpec(msg.getVolumeLuksAgentSpec()); if (msg.getHostUuid() != null) { imsg.setDestHost(HostInventory.valueOf(dbf.findByUuid(msg.getHostUuid(), HostVO.class))); } @@ -3374,6 +3380,46 @@ public void fail(ErrorCode errorCode) { } } + /** + * Converts this volume's bits to LUKS-encrypted form in place. The heavy lifting + * (key materialization, host secret staging, PS-side LUKS conversion, DB update) + * is delegated to {@link VolumeInPlaceEncryptor} so both this message handler and + * {@code VolumeManagerImpl}'s create-data-volume-from-template flow share a single + * implementation. + */ + private void handle(EncryptVolumeMsg msg) { + EncryptVolumeReply reply = new EncryptVolumeReply(); + refreshVO(); + + if (self == null) { + reply.setError(operr("volume[uuid:%s] has been deleted", msg.getVolumeUuid())); + bus.reply(msg, reply); + return; + } + + VolumeInPlaceEncryptor.Context ctx = new VolumeInPlaceEncryptor.Context() + .setHostUuid(msg.getHostUuid()) + .setPrimaryStorageUuid(msg.getPrimaryStorageUuid()) + .setInstallPath(msg.getInstallPath()) + .setPurpose(msg.getPurpose()) + .setForce(msg.isForce()); + + volumeInPlaceEncryptor.encryptInPlace(self, ctx, new ReturnValueCompletion(msg) { + @Override + public void success(VolumeVO latest) { + self = latest; + reply.setInventory(getSelfInventory()); + bus.reply(msg, reply); + } + + @Override + public void fail(ErrorCode errorCode) { + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } + @VmAttachVolumeValidatorMethod static void vmAttachVolumeValidator(VmInstanceInventory vmInv, String volumeUuid) { String vmUuid = vmInv.getUuid(); diff --git a/storage/src/main/java/org/zstack/storage/volume/VolumeInPlaceEncryptor.java b/storage/src/main/java/org/zstack/storage/volume/VolumeInPlaceEncryptor.java new file mode 100644 index 00000000000..430607cd704 --- /dev/null +++ b/storage/src/main/java/org/zstack/storage/volume/VolumeInPlaceEncryptor.java @@ -0,0 +1,281 @@ +package org.zstack.storage.volume; + +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Configurable; +import org.zstack.core.cloudbus.CloudBus; +import org.zstack.core.cloudbus.CloudBusCallBack; +import org.zstack.core.db.DatabaseFacade; +import org.zstack.header.core.FutureReturnValueCompletion; +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.header.host.HostConstant; +import org.zstack.header.keyprovider.EncryptedResourceKeyManager; +import org.zstack.header.message.MessageReply; +import org.zstack.header.secret.SecretHostEnsureLuksSecretFileMsg; +import org.zstack.header.secret.SecretHostEnsureLuksSecretFileReply; +import org.zstack.header.storage.primary.EncryptVolumeBitsOnPrimaryStorageMsg; +import org.zstack.header.storage.primary.PrimaryStorageConstant; +import org.zstack.header.volume.VolumeVO; +import org.zstack.storage.encrypt.VolumeEncryptedResourceKeyBackend; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.concurrent.TimeUnit; + +import static org.zstack.core.Platform.operr; + +/** + * Performs an in-place LUKS conversion of an existing volume's bits. + * + *

This is the single source of truth for the "encrypt-in-place" workflow that was + * previously duplicated between {@link VolumeBase#handleMessage} (for the + * {@code EncryptVolumeMsg} entry point) and + * {@link VolumeManagerImpl} (for the create-data-volume-from-template flow). + * + *

Steps performed: + *

    + *
  1. Ensure a key-provider binding exists for the volume; auto-attach the default + * provider when none is bound yet.
  2. + *
  3. Materialize a DEK via {@link EncryptedResourceKeyManager#getOrCreateKey}.
  4. + *
  5. Stage the LUKS secret material file on the target host via + * {@code SecretHostEnsureLuksSecretFileMsg}.
  6. + *
  7. Ask the primary storage backend to LUKS-convert the bits in place via + * {@code EncryptVolumeBitsOnPrimaryStorageMsg}.
  8. + *
  9. Persist {@code VolumeVO.encrypted = true} when it wasn't already.
  10. + *
+ * + *

Idempotency: when {@code volume.encrypted == true} and {@code force == false} the + * helper short-circuits and reports success with the unchanged volume row. Callers that + * pre-marked the row encrypted before writing plain bits (e.g. the create-data-volume- + * from-template flow) must pass {@code force = true} so the conversion runs. + */ +@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) +public class VolumeInPlaceEncryptor { + private static final CLogger logger = Utils.getLogger(VolumeInPlaceEncryptor.class); + + @Autowired + private CloudBus bus; + @Autowired + private DatabaseFacade dbf; + @Autowired(required = false) + private EncryptedResourceKeyManager encryptedResourceKeyManager; + @Autowired(required = false) + private VolumeEncryptedResourceKeyBackend volumeEncryptedResourceKeyBackend; + + /** + * Inputs that don't live on {@link VolumeVO} (host to stage the secret on, overrides + * for installPath / primaryStorageUuid when the caller already knows them, etc.). + */ + public static class Context { + private String hostUuid; + /** Optional; falls back to {@code volume.getPrimaryStorageUuid()}. */ + private String primaryStorageUuid; + /** Optional; falls back to {@code volume.getInstallPath()}. */ + private String installPath; + /** Free-form purpose label for the DEK get-or-create audit trail. */ + private String purpose; + /** When true, runs the conversion even if the volume is already marked encrypted. */ + private boolean force; + + public String getHostUuid() { + return hostUuid; + } + + public Context setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + return this; + } + + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public Context setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + return this; + } + + public String getInstallPath() { + return installPath; + } + + public Context setInstallPath(String installPath) { + this.installPath = installPath; + return this; + } + + public String getPurpose() { + return purpose; + } + + public Context setPurpose(String purpose) { + this.purpose = purpose; + return this; + } + + public boolean isForce() { + return force; + } + + public Context setForce(boolean force) { + this.force = force; + return this; + } + } + + /** + * Run the encrypt-in-place workflow. + * + * @param volume the (already-persisted) target volume; the caller is responsible + * for having a fresh row before invoking + * @param ctx host / installPath / purpose / force flags + * @param completion success returns the latest {@code VolumeVO} (encrypted row when + * the workflow actually ran, or the original row when it was a + * no-op short-circuit) + */ + public void encryptInPlace(VolumeVO volume, Context ctx, ReturnValueCompletion completion) { + if (volume == null) { + completion.fail(operr("encrypt-in-place: volume is null")); + return; + } + + // Idempotent short-circuit: already encrypted and caller didn't ask for a forced re-run. + if (volume.isEncrypted() && !ctx.isForce()) { + completion.success(volume); + return; + } + + if (encryptedResourceKeyManager == null || volumeEncryptedResourceKeyBackend == null) { + completion.fail(operr( + "cannot encrypt volume[uuid:%s] in place: encryption components are not installed", + volume.getUuid())); + return; + } + + if (StringUtils.isBlank(ctx.getHostUuid())) { + completion.fail(operr( + "cannot encrypt volume[uuid:%s] in place: hostUuid is required to stage LUKS secret", + volume.getUuid())); + return; + } + + final String installPath = StringUtils.isNotBlank(ctx.getInstallPath()) + ? ctx.getInstallPath() : volume.getInstallPath(); + if (StringUtils.isBlank(installPath)) { + completion.fail(operr( + "cannot encrypt volume[uuid:%s] in place: installPath unknown (volume not instantiated?)", + volume.getUuid())); + return; + } + + final String psUuid = StringUtils.isNotBlank(ctx.getPrimaryStorageUuid()) + ? ctx.getPrimaryStorageUuid() : volume.getPrimaryStorageUuid(); + if (StringUtils.isBlank(psUuid)) { + completion.fail(operr( + "cannot encrypt volume[uuid:%s] in place: primaryStorageUuid unknown", + volume.getUuid())); + return; + } + + // 1) Ensure a key-provider binding exists; auto-attach the default provider when missing. + // Binding is no longer eagerly performed at volume-create time, so this helper is the + // canonical attach point for the encrypt-from-template and encrypt-existing-volume paths + // (the regular instantiate path is covered by VolumeEncryptedInitialExtension). + String kpUuid = volumeEncryptedResourceKeyBackend.findKeyProviderUuidByVolume(volume.getUuid()); + if (StringUtils.isBlank(kpUuid)) { + kpUuid = volumeEncryptedResourceKeyBackend.defaultKeyProviderUuid(); + if (StringUtils.isBlank(kpUuid)) { + completion.fail(operr( + "cannot encrypt volume[uuid:%s] in place: no key provider bound and no default key provider configured", + volume.getUuid())); + return; + } + volumeEncryptedResourceKeyBackend.attachKeyProviderToVolume(volume.getUuid(), kpUuid); + } + + // 2) Materialize the DEK (idempotent: get-or-create). + EncryptedResourceKeyManager.GetOrCreateResourceKeyContext keyCtx = + new EncryptedResourceKeyManager.GetOrCreateResourceKeyContext(); + keyCtx.setResourceUuid(volume.getUuid()); + keyCtx.setResourceType(VolumeVO.class.getSimpleName()); + keyCtx.setKeyProviderUuid(kpUuid); + keyCtx.setPurpose(StringUtils.defaultIfBlank(ctx.getPurpose(), "encrypt-volume-in-place")); + + FutureReturnValueCompletion keyFuture = new FutureReturnValueCompletion(null); + encryptedResourceKeyManager.getOrCreateKey(keyCtx, keyFuture); + keyFuture.await(TimeUnit.MINUTES.toMillis(2)); + if (!keyFuture.isSuccess()) { + completion.fail(operr( + "failed to materialize encryption key for volume[uuid:%s]: %s", + volume.getUuid(), keyFuture.getErrorCode())); + return; + } + EncryptedResourceKeyManager.ResourceKeyResult keyResult = keyFuture.getResult(); + final String dekBase64 = keyResult.getDekBase64(); + if (StringUtils.isBlank(dekBase64)) { + completion.fail(operr( + "encryption key manager returned empty DEK for volume[uuid:%s]", + volume.getUuid())); + return; + } + + // 3) Stage the LUKS secret material file on the host. + SecretHostEnsureLuksSecretFileMsg ensureMsg = new SecretHostEnsureLuksSecretFileMsg(); + ensureMsg.setHostUuid(ctx.getHostUuid()); + ensureMsg.setDekBase64(dekBase64); + bus.makeTargetServiceIdByResourceUuid(ensureMsg, HostConstant.SERVICE_ID, ctx.getHostUuid()); + + FutureReturnValueCompletion secretFuture = new FutureReturnValueCompletion(null); + bus.send(ensureMsg, new CloudBusCallBack(secretFuture) { + @Override + public void run(MessageReply r) { + if (!r.isSuccess()) { + secretFuture.fail(r.getError()); + return; + } + SecretHostEnsureLuksSecretFileReply er = r.castReply(); + if (StringUtils.isBlank(er.getSecFilePath())) { + secretFuture.fail(operr( + "ensure LUKS secret file on host succeeded but secFilePath is empty, host[uuid:%s]", + ctx.getHostUuid())); + return; + } + secretFuture.success(er.getSecFilePath()); + } + }); + secretFuture.await(TimeUnit.MINUTES.toMillis(2)); + if (!secretFuture.isSuccess()) { + completion.fail(operr( + "failed to prepare LUKS secret material file for volume[uuid:%s] on host[uuid:%s]: %s", + volume.getUuid(), ctx.getHostUuid(), secretFuture.getErrorCode())); + return; + } + final String secFilePath = (String) secretFuture.getResult(); + + // 4) Ask the PS backend to LUKS-convert the bits in place. + EncryptVolumeBitsOnPrimaryStorageMsg emsg = new EncryptVolumeBitsOnPrimaryStorageMsg(); + emsg.setPrimaryStorageUuid(psUuid); + emsg.setHostUuid(ctx.getHostUuid()); + emsg.setVolumeUuid(volume.getUuid()); + emsg.setInstallPath(installPath); + emsg.setEncryptLuksSecretMaterialFilePath(secFilePath); + bus.makeTargetServiceIdByResourceUuid(emsg, PrimaryStorageConstant.SERVICE_ID, psUuid); + bus.send(emsg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply r) { + if (!r.isSuccess()) { + completion.fail(r.getError()); + return; + } + // 5) Persist encrypted=true when the caller hadn't already done so. + VolumeVO latest = volume; + if (!latest.isEncrypted()) { + latest.setEncrypted(true); + latest = dbf.updateAndRefresh(latest); + } + completion.success(latest); + } + }); + } +} diff --git a/storage/src/main/java/org/zstack/storage/volume/VolumeManagerImpl.java b/storage/src/main/java/org/zstack/storage/volume/VolumeManagerImpl.java index 88aa63237c5..235f43a98b0 100755 --- a/storage/src/main/java/org/zstack/storage/volume/VolumeManagerImpl.java +++ b/storage/src/main/java/org/zstack/storage/volume/VolumeManagerImpl.java @@ -92,6 +92,8 @@ public class VolumeManagerImpl extends AbstractService implements VolumeManager, private PluginRegistry pluginRgty; @Autowired private VmInstanceResourceMetadataManager vidm; + @Autowired + private VolumeInPlaceEncryptor volumeInPlaceEncryptor; private Future volumeExpungeTask; @@ -239,6 +241,7 @@ private void handle(CreateDataVolumeFromVolumeTemplateMsg msg) { vol.setPrimaryStorageUuid(msg.getPrimaryStorageUuid()); vol.setAccountUuid(msg.getAccountUuid()); vol.setShareable(getShareableCapabilityFromMsg(msg)); + vol.setEncrypted(msg.getEncrypted()); if (msg.getSystemTags() != null) { Iterator iterators = msg.getSystemTags().iterator(); @@ -461,6 +464,43 @@ public void rollback(FlowRollback trigger, Map data) { } }); + flow(new NoRollbackFlow() { + String __name__ = String.format("encrypt data volume %s in place if needed", vol.getUuid()); + + @Override + public boolean skip(Map data) { + // Only run when the volume is marked encrypted on the message. + // The downstream helper (VolumeInPlaceEncryptor) lazily attaches the + // default key provider when this volume isn't yet bound. + return !Boolean.TRUE.equals(msg.getEncrypted()); + } + + @Override + public void run(FlowTrigger trigger, Map data) { + // Delegate to the shared encrypt-in-place helper. force=true because the + // VolumeVO row was already marked encrypted earlier (so the idempotent + // short-circuit in the helper would otherwise skip the conversion), but + // the bits we just downloaded are still plain. + VolumeInPlaceEncryptor.Context ctx = new VolumeInPlaceEncryptor.Context() + .setHostUuid(msg.getHostUuid()) + .setPrimaryStorageUuid(targetPrimaryStorage.getUuid()) + .setInstallPath(primaryStorageInstallPath) + .setPurpose("create-data-volume-from-template") + .setForce(true); + volumeInPlaceEncryptor.encryptInPlace(vol, ctx, new ReturnValueCompletion(trigger) { + @Override + public void success(VolumeVO latest) { + trigger.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + trigger.fail(errorCode); + } + }); + } + }); + flow(new NoRollbackFlow() { String __name__ = String.format("sync volume %s size", vol.getUuid()); @@ -586,6 +626,7 @@ private VolumeInventory createVolume(CreateVolumeMsg msg) { vo.setStatus(VolumeStatus.NotInstantiated); vo.setType(VolumeType.valueOf(msg.getVolumeType())); vo.setDiskOfferingUuid(msg.getDiskOfferingUuid()); + vo.setEncrypted(msg.getEncrypted()); if (vo.getType() == VolumeType.Root) { vo.setDeviceId(0); }