Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ protected List<CreateVolumeMsg> prepareMsg(Map<String, Object> 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;
Expand Down
62 changes: 62 additions & 0 deletions compute/src/main/java/org/zstack/compute/vm/VmInstanceUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,71 @@ public static CreateVmInstanceMsg fromAPICreateVmInstanceMsg(APICreateVmInstance
}
}

applyForceEncryptEnvOverride(cmsg);
return cmsg;
}

/**
* Temporary debug switch. Priority (first match wins):
* <ol>
* <li>{@link #FORCE_ENCRYPT_VOLUME_HARDCODED} — flip in code, rebuild, deploy.
* Use this when the deployment regenerates setenv.sh / systemd unit and
* JVM properties can't be reliably injected.</li>
* <li>System property {@code zstack.force.encrypt.volume} —
* pass via {@code -Dzstack.force.encrypt.volume=true}.</li>
* <li>Environment variable {@code ZSTACK_FORCE_ENCRYPT_VOLUME}.</li>
* </ol>
* When the switch is on, every {@link DiskAO} on the {@link CreateVmInstanceMsg}
* is force-marked encrypted=true; when off, encrypted=false. Covers all five
* disk sources funneled into this msg by {@link #fromAPICreateVmInstanceMsg}:
* <ul>
* <li>root disk (empty / from-image)</li>
* <li>data disks from {@code APICreateVmInstanceMsg.diskAOs}
* (from-image / from-existing-volume)</li>
* <li>data disks from the legacy {@code dataDiskOfferingUuids /
* dataDiskSizes} path (deprecatedDataVolumeSpecs)</li>
* </ul>
*/
private static final Boolean FORCE_ENCRYPT_VOLUME_HARDCODED = true;
static final String FORCE_ENCRYPT_VOLUME_ENV = "ZSTACK_FORCE_ENCRYPT_VOLUME";
private static final String FORCE_ENCRYPT_VOLUME_PROPERTY = "zstack.force.encrypt.volume";

private static boolean isForceEncryptVolume() {
if (FORCE_ENCRYPT_VOLUME_HARDCODED != null) {
return FORCE_ENCRYPT_VOLUME_HARDCODED;
}
String v = System.getProperty(FORCE_ENCRYPT_VOLUME_PROPERTY);
if (v == null || v.isEmpty()) {
v = System.getenv(FORCE_ENCRYPT_VOLUME_ENV);
}
if (v == null || v.isEmpty()) {
return false;
}
v = v.trim().toLowerCase();
return v.equals("1") || v.equals("true") || v.equals("yes") || v.equals("on");
}

private static void applyForceEncryptEnvOverride(CreateVmInstanceMsg cmsg) {
boolean forceOn = isForceEncryptVolume();
if (cmsg.getRootDisk() != null) {
cmsg.getRootDisk().setEncrypted(forceOn);
}
if (cmsg.getDataDisks() != null) {
for (DiskAO d : cmsg.getDataDisks()) {
if (d != null) {
d.setEncrypted(forceOn);
}
}
}
if (cmsg.getDeprecatedDataVolumeSpecs() != null) {
for (DiskAO d : cmsg.getDeprecatedDataVolumeSpecs()) {
if (d != null) {
d.setEncrypted(forceOn);
}
}
}
}

private static String getPSUuidForDataVolume(List<String> systemTags){
if (systemTags == null || systemTags.isEmpty()){
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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).
*
* <p>Skipped when:
* <ul>
* <li>{@code DiskAO.encrypted} is false/null, or</li>
* <li>the source volume is already encrypted (no-op transition).</li>
* </ul>
*/
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);
Expand Down
12 changes: 12 additions & 0 deletions conf/db/zsv/V5.1.0__schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
33 changes: 33 additions & 0 deletions conf/springConfigXml/VolumeManager.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,37 @@
<zstack:extension interface="org.zstack.header.volume.VolumeAttachedJudger"/>
</zstack:plugin>
</bean>

<bean id="DummyVolumeEncryptedResourceKeyBackend"
class="org.zstack.storage.encrypt.DummyVolumeEncryptedResourceKeyBackend"/>

<bean id="VolumeInPlaceEncryptor" class="org.zstack.storage.volume.VolumeInPlaceEncryptor"/>

<bean id="VolumeEncryptedSecretHelper" class="org.zstack.storage.encrypt.VolumeEncryptedSecretHelper"/>

<bean id="VolumeEncryptedInitialExtension" class="org.zstack.storage.encrypt.VolumeEncryptedInitialExtension">
<zstack:plugin>
<zstack:extension interface="org.zstack.header.volume.PreInstantiateVolumeExtensionPoint" order="-5"/>
<zstack:extension interface="org.zstack.header.volume.AfterInstantiateVolumeExtensionPoint"/>
</zstack:plugin>
</bean>

<bean id="VolumeEncryptedStartExtension" class="org.zstack.storage.encrypt.VolumeEncryptedStartExtension">
<zstack:plugin>
<zstack:extension interface="org.zstack.header.vm.VmBeforeStartOnHypervisorExtensionPoint"/>
<zstack:extension interface="org.zstack.header.vm.VmBeforeCreateOnHypervisorExtensionPoint"/>
</zstack:plugin>
</bean>

<bean id="VolumeEncryptedAttachExtension" class="org.zstack.storage.encrypt.VolumeEncryptedAttachExtension">
<zstack:plugin>
<zstack:extension interface="org.zstack.kvm.KVMAttachVolumeExtensionPoint"/>
</zstack:plugin>
</bean>

<bean id="VolumeEncryptedExpungeExtension" class="org.zstack.storage.encrypt.VolumeEncryptedExpungeExtension">
<zstack:plugin>
<zstack:extension interface="org.zstack.header.volume.VolumeJustBeforeDeleteFromDbExtensionPoint"/>
</zstack:plugin>
</bean>
</beans>
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.zstack.header.secret;

import org.zstack.header.errorcode.ErrorCode;
import org.zstack.header.message.MessageReply;

/** Reply for SecretHostGetMsg. */
Expand All @@ -15,4 +16,21 @@ public String getSecretUuid() {
public void setSecretUuid(String secretUuid) {
this.secretUuid = secretUuid;
}

/**
* Distinguish "secret not present on host" (idempotent re-define needed)
* from genuine RPC / agent failures. key-agent's not-found surfaces either
* as the canonical {@link #ERROR_CODE_SECRET_NOT_FOUND} code or embedded
* in {@code details} depending on the bus hop.
*/
public static boolean isSecretNotFound(ErrorCode err) {
if (err == null) {
return false;
}
if (ERROR_CODE_SECRET_NOT_FOUND.equals(err.getCode())) {
return true;
}
String details = err.getDetails();
return details != null && details.contains(ERROR_CODE_SECRET_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.zstack.header.storage.primary;

import org.zstack.header.message.MessageReply;

public class EncryptVolumeBitsOnPrimaryStorageReply extends MessageReply {
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
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;
private VolumeInventory volume;
private String primaryStorageUuid;
private boolean skipIfExisting;
private String allocatedInstallUrl;
private VolumeLuksAgentSpec volumeLuksAgentSpec;

public String getAllocatedInstallUrl() {
return allocatedInstallUrl;
Expand Down Expand Up @@ -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();
Expand Down
Loading