diff --git a/api/src/main/java/org/apache/cloudstack/ca/CAManager.java b/api/src/main/java/org/apache/cloudstack/ca/CAManager.java index b0fb1ac73c2..2c1f9ae8d20 100644 --- a/api/src/main/java/org/apache/cloudstack/ca/CAManager.java +++ b/api/src/main/java/org/apache/cloudstack/ca/CAManager.java @@ -39,7 +39,10 @@ public interface CAManager extends CAService, Configurable, PluggableService { ConfigKey CAProviderPlugin = new ConfigKey<>("Advanced", String.class, "ca.framework.provider.plugin", "root", - "The CA provider plugin that is used for secure CloudStack management server-agent communication for encryption and authentication. Restart management server(s) when changed.", true); + "The CA provider plugin used for CloudStack internal certificate management (MS-agent encryption and authentication). " + + "The default 'root' provider auto-generates a CA on first startup, but also supports user-provided custom CA material " + + "via the ca.plugin.root.private.key, ca.plugin.root.public.key, and ca.plugin.root.ca.certificate settings. " + + "Restart management server(s) when changed.", true); ConfigKey CertKeySize = new ConfigKey<>("Advanced", Integer.class, "ca.framework.cert.keysize", diff --git a/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java b/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java index 25c45ed2a10..15f7f46d1b1 100644 --- a/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java +++ b/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCAProvider.java @@ -106,17 +106,21 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con private static ConfigKey rootCAPrivateKey = new ConfigKey<>("Hidden", String.class, "ca.plugin.root.private.key", null, - "The ROOT CA private key.", true); + "The ROOT CA private key in PEM format (PKCS#8: must start with '-----BEGIN PRIVATE KEY-----'). " + + "When set along with the public key and certificate, CloudStack uses this custom CA instead of auto-generating one. " + + "All three ca.plugin.root.* keys must be set together. Restart management server(s) when changed.", true); private static ConfigKey rootCAPublicKey = new ConfigKey<>("Hidden", String.class, "ca.plugin.root.public.key", null, - "The ROOT CA public key.", true); + "The ROOT CA public key in PEM format (X.509/SPKI: must start with '-----BEGIN PUBLIC KEY-----'). " + + "Required when providing a custom CA. Restart management server(s) when changed.", true); private static ConfigKey rootCACertificate = new ConfigKey<>("Hidden", String.class, "ca.plugin.root.ca.certificate", null, - "The ROOT CA certificate.", true); + "The ROOT CA X.509 certificate in PEM format (must start with '-----BEGIN CERTIFICATE-----'). " + + "Required when providing a custom CA. Restart management server(s) when changed.", true); private static ConfigKey rootCAIssuerDN = new ConfigKey<>("Advanced", String.class, "ca.plugin.root.issuer.dn", @@ -422,13 +426,29 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con private boolean setupCA() { - if (!loadRootCAKeyPair() && !saveNewRootCAKeypair()) { - logger.error("Failed to save and load root CA keypair"); - return false; + if (!loadRootCAKeyPair()) { + if (hasUserProvidedCAKeys()) { + logger.error("Failed to load user-provided CA keys from configuration. " + + "Check that ca.plugin.root.private.key, ca.plugin.root.public.key, and " + + "ca.plugin.root.ca.certificate are all set and in the correct PEM format " + + "(private key must be PKCS#8: '-----BEGIN PRIVATE KEY-----'). " + + "Overwriting with auto-generated keys."); + } + if (!saveNewRootCAKeypair()) { + logger.error("Failed to save and load root CA keypair"); + return false; + } } - if (!loadRootCACertificate() && !saveNewRootCACertificate()) { - logger.error("Failed to save and load root CA certificate"); - return false; + if (!loadRootCACertificate()) { + if (hasUserProvidedCAKeys()) { + logger.error("Failed to load user-provided CA certificate. " + + "Check that ca.plugin.root.ca.certificate is set and in PEM format. " + + "Overwriting with auto-generated certificate."); + } + if (!saveNewRootCACertificate()) { + logger.error("Failed to save and load root CA certificate"); + return false; + } } if (!loadManagementKeyStore()) { logger.error("Failed to check and configure management server keystore"); @@ -437,10 +457,16 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con return true; } + private boolean hasUserProvidedCAKeys() { + return StringUtils.isNotEmpty(rootCAPublicKey.value()) + || StringUtils.isNotEmpty(rootCAPrivateKey.value()) + || StringUtils.isNotEmpty(rootCACertificate.value()); + } + @Override public boolean start() { managementCertificateCustomSAN = CAManager.CertManagementCustomSubjectAlternativeName.value(); - return loadRootCAKeyPair() && loadRootCAKeyPair() && loadManagementKeyStore(); + return loadRootCAKeyPair() && loadRootCACertificate() && loadManagementKeyStore(); } @Override diff --git a/scripts/util/keystore-cert-import b/scripts/util/keystore-cert-import index a9465f273a3..4b718bfabac 100755 --- a/scripts/util/keystore-cert-import +++ b/scripts/util/keystore-cert-import @@ -137,6 +137,22 @@ if [ -f "$SYSTEM_FILE" ]; then chmod 644 /usr/local/share/ca-certificates/cloudstack/ca.crt update-ca-certificates > /dev/null 2>&1 || true + # Import CA cert(s) into realhostip.keystore so the SSVM JVM + # (which overrides the truststore via -Djavax.net.ssl.trustStore in _run.sh) + # can trust servers signed by the CloudStack CA + REALHOSTIP_KS_FILE="$(dirname $(dirname $PROPS_FILE))/certs/realhostip.keystore" + REALHOSTIP_PASS="vmops.com" + if [ -f "$REALHOSTIP_KS_FILE" ]; then + awk '/-----BEGIN CERTIFICATE-----/{n++}{print > "cloudca." n }' "$CACERT_FILE" + for caChain in $(ls cloudca.* 2>/dev/null); do + keytool -delete -noprompt -alias "$caChain" -keystore "$REALHOSTIP_KS_FILE" \ + -storepass "$REALHOSTIP_PASS" > /dev/null 2>&1 || true + keytool -import -noprompt -trustcacerts -alias "$caChain" -file "$caChain" \ + -keystore "$REALHOSTIP_KS_FILE" -storepass "$REALHOSTIP_PASS" > /dev/null 2>&1 + done + rm -f cloudca.* + fi + # Ensure cloud service is running in systemvm if [ "$MODE" == "ssh" ]; then systemctl start cloud > /dev/null 2>&1 diff --git a/systemvm/patch-sysvms.sh b/systemvm/patch-sysvms.sh index 88d720e0f32..9e604d26766 100755 --- a/systemvm/patch-sysvms.sh +++ b/systemvm/patch-sysvms.sh @@ -126,7 +126,28 @@ patch_systemvm() { if [ "$TYPE" = "consoleproxy" ] || [ "$TYPE" = "secstorage" ]; then # Import global cacerts into 'cloud' service's keystore - keytool -importkeystore -srckeystore /etc/ssl/certs/java/cacerts -destkeystore /usr/local/cloud/systemvm/certs/realhostip.keystore -srcstorepass changeit -deststorepass vmops.com -noprompt 2>/dev/null || true + REALHOSTIP_KS_FILE="/usr/local/cloud/systemvm/certs/realhostip.keystore" + REALHOSTIP_PASS="vmops.com" + + keytool -importkeystore -srckeystore /etc/ssl/certs/java/cacerts \ + -destkeystore "$REALHOSTIP_KS_FILE" -srcstorepass changeit -deststorepass \ + "$REALHOSTIP_PASS" -noprompt 2>/dev/null || true + + # Import CA cert(s) into realhostip.keystore so the SSVM JVM + # (which overrides the truststore via -Djavax.net.ssl.trustStore in _run.sh) + # can trust servers signed by the CloudStack CA + CACERT_FILE="/usr/local/share/ca-certificates/cloudstack/ca.crt" + + if [ -f "$CACERT_FILE" ] && [ -f "$REALHOSTIP_KS_FILE" ]; then + awk '/-----BEGIN CERTIFICATE-----/{n++}{print > "cloudca." n }' "$CACERT_FILE" + for caChain in $(ls cloudca.* 2>/dev/null); do + keytool -delete -noprompt -alias "$caChain" -keystore "$REALHOSTIP_KS_FILE" \ + -storepass "$REALHOSTIP_PASS" > /dev/null 2>&1 || true + keytool -import -noprompt -trustcacerts -alias "$caChain" -file "$caChain" \ + -keystore "$REALHOSTIP_KS_FILE" -storepass "$REALHOSTIP_PASS" > /dev/null 2>&1 + done + rm -f cloudca.* + fi fi update_checksum $newpath/cloud-scripts.tgz diff --git a/utils/src/main/java/com/cloud/utils/nio/Link.java b/utils/src/main/java/com/cloud/utils/nio/Link.java index 18bbb0533ee..71bb0b7eda5 100644 --- a/utils/src/main/java/com/cloud/utils/nio/Link.java +++ b/utils/src/main/java/com/cloud/utils/nio/Link.java @@ -552,7 +552,7 @@ public class Link { LOGGER.error(String.format("SSL error caught during wrap data: %s, for local address=%s, remote address=%s.", sslException.getMessage(), socketChannel.getLocalAddress(), socketChannel.getRemoteAddress())); sslEngine.closeOutbound(); - return new HandshakeHolder(myAppData, myNetData, true); + return new HandshakeHolder(myAppData, myNetData, false); } if (result == null) { return new HandshakeHolder(myAppData, myNetData, false); diff --git a/utils/src/main/java/org/apache/cloudstack/utils/security/KeyStoreUtils.java b/utils/src/main/java/org/apache/cloudstack/utils/security/KeyStoreUtils.java index e78d14adbb2..c6f8d21918c 100644 --- a/utils/src/main/java/org/apache/cloudstack/utils/security/KeyStoreUtils.java +++ b/utils/src/main/java/org/apache/cloudstack/utils/security/KeyStoreUtils.java @@ -26,7 +26,6 @@ import com.cloud.utils.PropertiesUtil; public class KeyStoreUtils { public static final String KS_SETUP_SCRIPT = "keystore-setup"; public static final String KS_IMPORT_SCRIPT = "keystore-cert-import"; - public static final String KS_SYSTEMVM_IMPORT_SCRIPT = "keystore-cert-import-sysvm"; public static final String AGENT_PROPSFILE = "agent.properties"; public static final String KS_PASSPHRASE_PROPERTY = "keystore.passphrase";