This is an automated email from the ASF dual-hosted git repository.

vishesh92 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new 1fe486f493b Add ROOT CAs to the trust store and allow force 
provisioning of certs hosts & systemVMs via ssh (#12911)
1fe486f493b is described below

commit 1fe486f493b2f52abcf971b3d009044848c7885f
Author: Vishesh <[email protected]>
AuthorDate: Thu May 21 13:19:13 2026 +0530

    Add ROOT CAs to the trust store and allow force provisioning of certs hosts 
& systemVMs via ssh (#12911)
---
 .../command/admin/ca/ProvisionCertificateCmd.java  |  12 +-
 .../java/org/apache/cloudstack/ca/CAManager.java   |  31 ++-
 .../ca/provider/RootCACustomTrustManager.java      |   8 +-
 .../cloudstack/ca/provider/RootCAProvider.java     | 172 ++++++++++++----
 .../ca/provider/RootCACustomTrustManagerTest.java  |  37 +++-
 .../cloudstack/ca/provider/RootCAProviderTest.java | 107 +++++++++-
 .../mom/webhook/WebhookDeliveryThread.java         |   6 +-
 scripts/util/keystore-cert-import                  |  20 +-
 .../kvm/discoverer/LibvirtServerDiscoverer.java    |  55 +----
 .../org/apache/cloudstack/ca/CAManagerImpl.java    | 223 ++++++++++++++++++++-
 .../apache/cloudstack/ca/CABackgroundTaskTest.java |  10 +-
 .../apache/cloudstack/ca/CAManagerImplTest.java    | 223 ++++++++++++++++++++-
 systemvm/patch-sysvms.sh                           |  23 ++-
 test/integration/smoke/test_certauthority_root.py  | 177 +++++++++++++++-
 ui/src/config/section/infra/hosts.js               |   2 +-
 utils/src/main/java/com/cloud/utils/nio/Link.java  |   2 +-
 .../cloudstack/utils/security/CertUtils.java       |  15 +-
 .../cloudstack/utils/security/KeyStoreUtils.java   |   1 -
 .../cloudstack/utils/security/CertUtilsTest.java   |  17 +-
 19 files changed, 989 insertions(+), 152 deletions(-)

diff --git 
a/api/src/main/java/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java
 
b/api/src/main/java/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java
index 6deaea22ac6..d333a74fdb3 100644
--- 
a/api/src/main/java/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java
+++ 
b/api/src/main/java/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java
@@ -63,6 +63,12 @@ public class ProvisionCertificateCmd extends BaseAsyncCmd {
             description = "Name of the CA service provider, otherwise the 
default configured provider plugin will be used")
     private String provider;
 
+    @Parameter(name = ApiConstants.FORCED, type = CommandType.BOOLEAN,
+            description = "When true, uses SSH to re-provision the agent's 
certificate, bypassing the NIO agent connection. " +
+            "Use this when agents are disconnected due to a CA change. 
Supported for KVM hosts and SystemVMs. Default is false",
+            since = "4.23.0")
+    private Boolean forced;
+
     /////////////////////////////////////////////////////
     /////////////////// Accessors ///////////////////////
     /////////////////////////////////////////////////////
@@ -79,6 +85,10 @@ public class ProvisionCertificateCmd extends BaseAsyncCmd {
         return provider;
     }
 
+    public boolean isForced() {
+        return forced != null && forced;
+    }
+
     /////////////////////////////////////////////////////
     /////////////// API Implementation///////////////////
     /////////////////////////////////////////////////////
@@ -90,7 +100,7 @@ public class ProvisionCertificateCmd extends BaseAsyncCmd {
             throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to 
find host by ID: " + getHostId());
         }
 
-        boolean result = caManager.provisionCertificate(host, getReconnect(), 
getProvider());
+        boolean result = caManager.provisionCertificate(host, getReconnect(), 
getProvider(), isForced());
         SuccessResponse response = new SuccessResponse(getCommandName());
         response.setSuccess(result);
         setResponseObject(response);
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..d2ebdc25f1b 100644
--- a/api/src/main/java/org/apache/cloudstack/ca/CAManager.java
+++ b/api/src/main/java/org/apache/cloudstack/ca/CAManager.java
@@ -23,6 +23,8 @@ import java.security.cert.X509Certificate;
 import java.util.List;
 import java.util.Map;
 
+import com.trilead.ssh2.Connection;
+
 import org.apache.cloudstack.framework.ca.CAProvider;
 import org.apache.cloudstack.framework.ca.CAService;
 import org.apache.cloudstack.framework.ca.Certificate;
@@ -39,7 +41,10 @@ public interface CAManager extends CAService, Configurable, 
PluggableService {
     ConfigKey<String> 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.", false);
 
     ConfigKey<Integer> CertKeySize = new ConfigKey<>("Advanced", Integer.class,
                                     "ca.framework.cert.keysize",
@@ -85,6 +90,12 @@ public interface CAManager extends CAService, Configurable, 
PluggableService {
                     "The actual implementation will depend on the configured 
CA provider.",
             false);
 
+    ConfigKey<Boolean> CaInjectDefaultTruststore = new ConfigKey<>("Advanced", 
Boolean.class,
+            "ca.framework.inject.default.truststore", "true",
+            "When true, injects the CA provider's certificate into the JVM 
default truststore on management server startup. " +
+            "This allows outgoing HTTPS connections from the management server 
to trust servers with certificates signed by the configured CA. " +
+            "Restart management server(s) when changed.", false);
+
     /**
      * Returns a list of available CA provider plugins
      * @return returns list of CAProvider
@@ -130,12 +141,26 @@ public interface CAManager extends CAService, 
Configurable, PluggableService {
     boolean revokeCertificate(final BigInteger certSerial, final String 
certCn, final String provider);
 
     /**
-     * Provisions certificate for given active and connected agent host
+     * Provisions certificate for given agent host.
+     * When forced=true, uses SSH to re-provision bypassing the NIO agent 
connection (for disconnected agents).
      * @param host
+     * @param reconnect
      * @param provider
+     * @param forced when true, provisions via SSH instead of NIO; supports 
KVM hosts and SystemVMs
      * @return returns success/failure as boolean
      */
-    boolean provisionCertificate(final Host host, final Boolean reconnect, 
final String provider);
+    boolean provisionCertificate(final Host host, final Boolean reconnect, 
final String provider, final boolean forced);
+
+    /**
+     * Provisions certificate for a KVM host using an existing SSH connection.
+     * Runs keystore-setup to generate a CSR, issues a certificate, then runs 
keystore-cert-import.
+     * Used during host discovery and for forced re-provisioning when the NIO 
agent is unreachable.
+     * @param sshConnection active SSH connection to the KVM host
+     * @param agentIp IP address of the KVM host agent
+     * @param agentHostname hostname of the KVM host agent
+     * @param caProvider optional CA provider plugin name (null uses default)
+     */
+    void provisionCertificateViaSsh(Connection sshConnection, String agentIp, 
String agentHostname, String caProvider);
 
     /**
      * Setups up a new keystore and generates CSR for a host
diff --git 
a/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java
 
b/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java
index 5ff036fef12..d018d488c64 100644
--- 
a/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java
+++ 
b/plugins/ca/root-ca/src/main/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java
@@ -40,17 +40,17 @@ public final class RootCACustomTrustManager implements 
X509TrustManager {
     private boolean authStrictness = true;
     private boolean allowExpiredCertificate = true;
     private CrlDao crlDao;
-    private X509Certificate caCertificate;
+    private List<X509Certificate> caCertificates;
     private Map<String, X509Certificate> activeCertMap;
 
-    public RootCACustomTrustManager(final String clientAddress, final boolean 
authStrictness, final boolean allowExpiredCertificate, final Map<String, 
X509Certificate> activeCertMap, final X509Certificate caCertificate, final 
CrlDao crlDao) {
+    public RootCACustomTrustManager(final String clientAddress, final boolean 
authStrictness, final boolean allowExpiredCertificate, final Map<String, 
X509Certificate> activeCertMap, final List<X509Certificate> caCertificates, 
final CrlDao crlDao) {
         if (StringUtils.isNotEmpty(clientAddress)) {
             this.clientAddress = clientAddress.replace("/", "").split(":")[0];
         }
         this.authStrictness = authStrictness;
         this.allowExpiredCertificate = allowExpiredCertificate;
         this.activeCertMap = activeCertMap;
-        this.caCertificate = caCertificate;
+        this.caCertificates = caCertificates;
         this.crlDao = crlDao;
     }
 
@@ -151,6 +151,6 @@ public final class RootCACustomTrustManager implements 
X509TrustManager {
 
     @Override
     public X509Certificate[] getAcceptedIssuers() {
-        return new X509Certificate[]{caCertificate};
+        return caCertificates.toArray(new X509Certificate[0]);
     }
 }
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..afb4f561160 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
@@ -40,7 +40,6 @@ import java.security.cert.X509Certificate;
 import java.security.spec.InvalidKeySpecException;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.List;
@@ -60,6 +59,7 @@ import org.apache.cloudstack.framework.ca.CAProvider;
 import org.apache.cloudstack.framework.ca.Certificate;
 import org.apache.cloudstack.framework.config.ConfigKey;
 import org.apache.cloudstack.framework.config.Configurable;
+import org.apache.cloudstack.framework.config.ValidatedConfigKey;
 import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
 import org.apache.cloudstack.utils.security.CertUtils;
 import org.apache.cloudstack.utils.security.KeyStoreUtils;
@@ -92,6 +92,7 @@ public final class RootCAProvider extends AdapterBase 
implements CAProvider, Con
 
     private static KeyPair caKeyPair = null;
     private static X509Certificate caCertificate = null;
+    private static List<X509Certificate> caCertificates = null;
     private static KeyStore managementKeyStore = null;
 
     @Inject
@@ -103,20 +104,25 @@ public final class RootCAProvider extends AdapterBase 
implements CAProvider, Con
     /////////////// Root CA Settings ///////////////////
     ////////////////////////////////////////////////////
 
-    private static ConfigKey<String> rootCAPrivateKey = new 
ConfigKey<>("Hidden", String.class,
-            "ca.plugin.root.private.key",
-            null,
-            "The ROOT CA private key.", true);
-
-    private static ConfigKey<String> rootCAPublicKey = new 
ConfigKey<>("Hidden", String.class,
-            "ca.plugin.root.public.key",
-            null,
-            "The ROOT CA public key.", true);
-
-    private static ConfigKey<String> rootCACertificate = new 
ConfigKey<>("Hidden", String.class,
-            "ca.plugin.root.ca.certificate",
-            null,
-            "The ROOT CA certificate.", true);
+    private static ConfigKey<String> rootCAPrivateKey = new 
ValidatedConfigKey<>("Hidden", String.class,
+            "ca.plugin.root.private.key", null,
+            "The ROOT CA private key in PEM format. " +
+            "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.",
+            false, ConfigKey.Scope.Global, null, 
RootCAProvider::validatePrivateKeyPem);
+
+    private static ConfigKey<String> rootCAPublicKey = new 
ValidatedConfigKey<>("Hidden", String.class,
+            "ca.plugin.root.public.key", null,
+            "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.",
+            false, ConfigKey.Scope.Global, null, 
RootCAProvider::validatePublicKeyPem);
+
+    private static ConfigKey<String> rootCACertificate = new 
ValidatedConfigKey<>("Hidden", String.class,
+            "ca.plugin.root.ca.certificate", null,
+            "The CA certificate(s) in PEM format (must start with '-----BEGIN 
CERTIFICATE-----'). " +
+            "For intermediate CAs, concatenate the signing cert first, 
followed by intermediate(s) and root. " +
+            "Required when providing a custom CA. Restart management server(s) 
when changed.",
+            false, ConfigKey.Scope.Global, null, 
RootCAProvider::validateCACertificatePem);
 
     private static ConfigKey<String> rootCAIssuerDN = new 
ConfigKey<>("Advanced", String.class,
             "ca.plugin.root.issuer.dn",
@@ -151,7 +157,7 @@ public final class RootCAProvider extends AdapterBase 
implements CAProvider, Con
                 caCertificate, caKeyPair, keyPair.getPublic(),
                 subject, CAManager.CertSignatureAlgorithm.value(),
                 validityDays, domainNames, ipAddresses);
-        return new Certificate(clientCertificate, keyPair.getPrivate(), 
Collections.singletonList(caCertificate));
+        return new Certificate(clientCertificate, keyPair.getPrivate(), 
caCertificates);
     }
 
     private Certificate generateCertificateUsingCsr(final String csr, final 
List<String> names, final List<String> ips, final int validityDays) throws 
NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, 
CertificateException, SignatureException, IOException, 
OperatorCreationException {
@@ -205,7 +211,7 @@ public final class RootCAProvider extends AdapterBase 
implements CAProvider, Con
                 caCertificate, caKeyPair, request.getPublicKey(),
                 subject, CAManager.CertSignatureAlgorithm.value(),
                 validityDays, dnsNames, ipAddresses);
-        return new Certificate(clientCertificate, null, 
Collections.singletonList(caCertificate));
+        return new Certificate(clientCertificate, null, caCertificates);
     }
 
     ////////////////////////////////////////////////////////
@@ -219,7 +225,7 @@ public final class RootCAProvider extends AdapterBase 
implements CAProvider, Con
 
     @Override
     public List<X509Certificate> getCaCertificate() {
-        return Collections.singletonList(caCertificate);
+        return caCertificates;
     }
 
     @Override
@@ -254,8 +260,8 @@ public final class RootCAProvider extends AdapterBase 
implements CAProvider, Con
     private KeyStore getCaKeyStore() throws CertificateException, 
NoSuchAlgorithmException, IOException, KeyStoreException {
         final KeyStore ks = KeyStore.getInstance("JKS");
         ks.load(null, null);
-        if (caKeyPair != null && caCertificate != null) {
-            ks.setKeyEntry(caAlias, caKeyPair.getPrivate(), 
getKeyStorePassphrase(), new X509Certificate[]{caCertificate});
+        if (caKeyPair != null && CollectionUtils.isNotEmpty(caCertificates)) {
+            ks.setKeyEntry(caAlias, caKeyPair.getPrivate(), 
getKeyStorePassphrase(), caCertificates.toArray(new X509Certificate[0]));
         } else {
             return null;
         }
@@ -274,7 +280,7 @@ public final class RootCAProvider extends AdapterBase 
implements CAProvider, Con
         final boolean authStrictness = rootCAAuthStrictness.value();
         final boolean allowExpiredCertificate = rootCAAllowExpiredCert.value();
 
-        TrustManager[] tms = new TrustManager[]{new 
RootCACustomTrustManager(remoteAddress, authStrictness, 
allowExpiredCertificate, certMap, caCertificate, crlDao)};
+        TrustManager[] tms = new TrustManager[]{new 
RootCACustomTrustManager(remoteAddress, authStrictness, 
allowExpiredCertificate, certMap, caCertificates, crlDao)};
 
         sslContext.init(kmf.getKeyManagers(), tms, new SecureRandom());
         final SSLEngine sslEngine = sslContext.createSSLEngine();
@@ -316,33 +322,39 @@ public final class RootCAProvider extends AdapterBase 
implements CAProvider, Con
             if (!configDao.update(rootCAPrivateKey.key(), 
rootCAPrivateKey.category(), CertUtils.privateKeyToPem(keyPair.getPrivate()))) {
                 logger.error("Failed to save RootCA private key");
             }
+            caKeyPair = keyPair;
         } catch (final NoSuchProviderException | NoSuchAlgorithmException | 
IOException e) {
             logger.error("Failed to generate/save RootCA private/public keys 
due to exception:", e);
         }
-        return loadRootCAKeyPair();
+        return caKeyPair != null && caKeyPair.getPrivate() != null && 
caKeyPair.getPublic() != null;
     }
 
-    private boolean saveNewRootCACertificate() {
+    boolean saveNewRootCACertificate() {
         if (caKeyPair == null) {
             throw new CloudRuntimeException("Cannot issue self-signed root CA 
certificate as CA keypair is not initialized");
         }
         try {
             logger.debug("Generating root CA certificate");
-            final X509Certificate rootCaCertificate = 
CertUtils.generateV3Certificate(
+            final X509Certificate generatedCACert = 
CertUtils.generateV3Certificate(
                     null, caKeyPair, caKeyPair.getPublic(),
                     rootCAIssuerDN.value(), 
CAManager.CertSignatureAlgorithm.value(),
                     getCaValidityDays(), null, null);
-            if (!configDao.update(rootCACertificate.key(), 
rootCACertificate.category(), 
CertUtils.x509CertificateToPem(rootCaCertificate))) {
+            if (!configDao.update(rootCACertificate.key(), 
rootCACertificate.category(), CertUtils.x509CertificateToPem(generatedCACert))) 
{
                 logger.error("Failed to update RootCA public/x509 
certificate");
             }
+            caCertificates = new 
ArrayList<>(java.util.Collections.singletonList(generatedCACert));
+            caCertificate = generatedCACert;
         } catch (final CertificateException | NoSuchAlgorithmException | 
NoSuchProviderException | SignatureException | InvalidKeyException | 
OperatorCreationException | IOException e) {
             logger.error("Failed to generate RootCA certificate from 
private/public keys due to exception:", e);
             return false;
         }
-        return loadRootCACertificate();
+        return caCertificate != null;
     }
 
     private boolean loadRootCAKeyPair() {
+        if (caKeyPair != null) {
+            return true;
+        }
         if (StringUtils.isAnyEmpty(rootCAPublicKey.value(), 
rootCAPrivateKey.value())) {
             return false;
         }
@@ -355,14 +367,35 @@ public final class RootCAProvider extends AdapterBase 
implements CAProvider, Con
         return caKeyPair.getPrivate() != null && caKeyPair.getPublic() != null;
     }
 
-    private boolean loadRootCACertificate() {
+    boolean loadRootCACertificate() {
+        if (caCertificate != null && 
CollectionUtils.isNotEmpty(caCertificates)) {
+            return true;
+        }
+        caCertificate = null;
+        caCertificates = null;
         if (StringUtils.isEmpty(rootCACertificate.value())) {
             return false;
         }
         try {
-            caCertificate = 
CertUtils.pemToX509Certificate(rootCACertificate.value());
-            caCertificate.verify(caKeyPair.getPublic());
-        } catch (final IOException | CertificateException | 
NoSuchAlgorithmException | InvalidKeyException | SignatureException | 
NoSuchProviderException e) {
+            final List<X509Certificate> loadedCerts = 
CertUtils.pemToX509Certificates(rootCACertificate.value());
+            if (CollectionUtils.isEmpty(loadedCerts)) {
+                logger.error("No certificates found in 
ca.plugin.root.ca.certificate");
+                return false;
+            }
+            final X509Certificate loadedCACert = loadedCerts.get(0);
+
+            // Verify key ownership without enforcing self-signature
+            if (!loadedCACert.getPublicKey().equals(caKeyPair.getPublic())) {
+                logger.error("The public key in the CA certificate does not 
match the configured CA public key");
+                return false;
+            }
+
+            if (loadedCerts.size() > 1) {
+                logger.info("Loaded CA certificate chain with {} 
certificate(s)", loadedCerts.size());
+            }
+            caCertificates = loadedCerts;
+            caCertificate = loadedCACert;
+        } catch (final IOException | CertificateException e) {
             logger.error("Failed to load saved RootCA certificate due to 
exception:", e);
             return false;
         }
@@ -389,9 +422,15 @@ public final class RootCAProvider extends AdapterBase 
implements CAProvider, Con
         try {
             managementKeyStore = KeyStore.getInstance("JKS");
             managementKeyStore.load(null, null);
-            managementKeyStore.setCertificateEntry(caAlias, caCertificate);
+            int caIndex = 0;
+            for (final X509Certificate cert : caCertificates) {
+                managementKeyStore.setCertificateEntry(caAlias + "-" + 
caIndex++, cert);
+            }
+            final List<X509Certificate> fullChain = new ArrayList<>();
+            fullChain.add(serverCertificate.getClientCertificate());
+            fullChain.addAll(caCertificates);
             managementKeyStore.setKeyEntry(managementAlias, 
serverCertificate.getPrivateKey(), getKeyStorePassphrase(),
-                    new 
X509Certificate[]{serverCertificate.getClientCertificate(), caCertificate});
+                    fullChain.toArray(new X509Certificate[0]));
         } catch (final CertificateException | NoSuchAlgorithmException | 
KeyStoreException | IOException  e) {
             logger.error("Failed to load root CA management-server keystore 
due to exception: ", e);
             return false;
@@ -421,14 +460,63 @@ public final class RootCAProvider extends AdapterBase 
implements CAProvider, Con
     }
 
 
+    private static void validatePrivateKeyPem(String value) {
+        if (StringUtils.isEmpty(value)) return;
+        try {
+            CertUtils.pemToPrivateKey(value);
+        } catch (InvalidKeySpecException | IOException e) {
+            throw new IllegalArgumentException(
+                    "ca.plugin.root.private.key is not a valid PEM private 
key: " + e.getMessage());
+        }
+    }
+
+    private static void validatePublicKeyPem(String value) {
+        if (StringUtils.isEmpty(value)) return;
+        try {
+            CertUtils.pemToPublicKey(value);
+        } catch (InvalidKeySpecException | IOException e) {
+            throw new IllegalArgumentException(
+                    "ca.plugin.root.public.key is not a valid PEM public key: 
" + e.getMessage());
+        }
+    }
+
+    static void validateCACertificatePem(String value) {
+        if (StringUtils.isEmpty(value)) return;
+        try {
+            final List<X509Certificate> certs = 
CertUtils.pemToX509Certificates(value);
+            if (CollectionUtils.isEmpty(certs)) {
+                throw new IllegalArgumentException(
+                        "ca.plugin.root.ca.certificate contains no 
certificates");
+            }
+        } catch (IOException | CertificateException e) {
+            throw new IllegalArgumentException(
+                    "ca.plugin.root.ca.certificate is not a valid PEM 
certificate: " + e.getMessage());
+        }
+    }
+
     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. " +
+                    "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 +525,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/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java
 
b/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java
index d4ded302332..714e18c3449 100644
--- 
a/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java
+++ 
b/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java
@@ -23,9 +23,11 @@ import java.math.BigInteger;
 import java.security.KeyPair;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.List;
 
 import org.apache.cloudstack.utils.security.CertUtils;
 import org.junit.Assert;
@@ -63,14 +65,14 @@ public class RootCACustomTrustManagerTest {
 
     @Test
     public void testAuthNotStrictWithInvalidCert() throws Exception {
-        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao);
+        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, false, true, certMap, 
Collections.singletonList(caCertificate), crlDao);
         trustManager.checkClientTrusted(null, null);
     }
 
     @Test
     public void testAuthNotStrictWithRevokedCert() throws Exception {
         
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(new 
CrlVO());
-        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao);
+        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, false, true, certMap, 
Collections.singletonList(caCertificate), crlDao);
         trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, 
"RSA");
         Assert.assertTrue(certMap.containsKey(clientIp));
         Assert.assertEquals(certMap.get(clientIp), caCertificate);
@@ -79,7 +81,7 @@ public class RootCACustomTrustManagerTest {
     @Test
     public void testAuthNotStrictWithInvalidCertOwnership() throws Exception {
         
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null);
-        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao);
+        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, false, true, certMap, 
Collections.singletonList(caCertificate), crlDao);
         trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, 
"RSA");
         Assert.assertTrue(certMap.containsKey(clientIp));
         Assert.assertEquals(certMap.get(clientIp), caCertificate);
@@ -88,14 +90,14 @@ public class RootCACustomTrustManagerTest {
     @Test(expected = CertificateException.class)
     public void testAuthNotStrictWithDenyExpiredCertAndOwnership() throws 
Exception {
         
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null);
-        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, false, false, certMap, caCertificate, 
crlDao);
+        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, false, false, certMap, 
Collections.singletonList(caCertificate), crlDao);
         trustManager.checkClientTrusted(new 
X509Certificate[]{expiredClientCertificate}, "RSA");
     }
 
     @Test
     public void testAuthNotStrictWithAllowExpiredCertAndOwnership() throws 
Exception {
         
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null);
-        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao);
+        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, false, true, certMap, 
Collections.singletonList(caCertificate), crlDao);
         trustManager.checkClientTrusted(new 
X509Certificate[]{expiredClientCertificate}, "RSA");
         Assert.assertTrue(certMap.containsKey(clientIp));
         Assert.assertEquals(certMap.get(clientIp), expiredClientCertificate);
@@ -103,35 +105,50 @@ public class RootCACustomTrustManagerTest {
 
     @Test(expected = CertificateException.class)
     public void testAuthStrictWithInvalidCert() throws Exception {
-        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao);
+        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, true, true, certMap, 
Collections.singletonList(caCertificate), crlDao);
         trustManager.checkClientTrusted(null, null);
     }
 
     @Test(expected = CertificateException.class)
     public void testAuthStrictWithRevokedCert() throws Exception {
         
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(new 
CrlVO());
-        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao);
+        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, true, true, certMap, 
Collections.singletonList(caCertificate), crlDao);
         trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, 
"RSA");
     }
 
     @Test(expected = CertificateException.class)
     public void testAuthStrictWithInvalidCertOwnership() throws Exception {
         
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null);
-        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao);
+        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, true, true, certMap, 
Collections.singletonList(caCertificate), crlDao);
         trustManager.checkClientTrusted(new X509Certificate[]{caCertificate}, 
"RSA");
     }
 
     @Test(expected = CertificateException.class)
     public void testAuthStrictWithDenyExpiredCertAndOwnership() throws 
Exception {
         
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null);
-        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, true, false, certMap, caCertificate, crlDao);
+        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, true, false, certMap, 
Collections.singletonList(caCertificate), crlDao);
         trustManager.checkClientTrusted(new 
X509Certificate[]{expiredClientCertificate}, "RSA");
     }
 
+    @Test
+    public void testGetAcceptedIssuersWithChain() throws Exception {
+        final KeyPair rootKeyPair = CertUtils.generateRandomKeyPair(1024);
+        final X509Certificate rootCert = CertUtils.generateV3Certificate(null, 
rootKeyPair, rootKeyPair.getPublic(),
+                "CN=root", "SHA256withRSA", 365, null, null);
+        final List<X509Certificate> chain = Arrays.asList(caCertificate, 
rootCert);
+        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(
+                clientIp, false, true, certMap, chain, crlDao);
+
+        final X509Certificate[] issuers = trustManager.getAcceptedIssuers();
+        Assert.assertEquals(2, issuers.length);
+        Assert.assertEquals(caCertificate, issuers[0]);
+        Assert.assertEquals(rootCert, issuers[1]);
+    }
+
     @Test
     public void testAuthStrictWithAllowExpiredCertAndOwnership() throws 
Exception {
         
Mockito.when(crlDao.findBySerial(Mockito.any(BigInteger.class))).thenReturn(null);
-        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, true, true, certMap, caCertificate, crlDao);
+        final RootCACustomTrustManager trustManager = new 
RootCACustomTrustManager(clientIp, true, true, certMap, 
Collections.singletonList(caCertificate), crlDao);
         Assert.assertTrue(trustManager.getAcceptedIssuers() != null);
         Assert.assertTrue(trustManager.getAcceptedIssuers().length == 1);
         Assert.assertEquals(trustManager.getAcceptedIssuers()[0], 
caCertificate);
diff --git 
a/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java
 
b/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java
index 8311f4d45ab..21f00c66a1d 100644
--- 
a/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java
+++ 
b/plugins/ca/root-ca/src/test/java/org/apache/cloudstack/ca/provider/RootCAProviderTest.java
@@ -31,6 +31,7 @@ import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
 
@@ -38,6 +39,7 @@ import javax.net.ssl.SSLEngine;
 
 import org.apache.cloudstack.framework.ca.Certificate;
 import org.apache.cloudstack.framework.config.ConfigKey;
+import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
 import org.apache.cloudstack.utils.security.CertUtils;
 import org.apache.cloudstack.utils.security.SSLUtils;
 import org.bouncycastle.asn1.x509.GeneralName;
@@ -49,7 +51,6 @@ import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnitRunner;
-import org.springframework.test.util.ReflectionTestUtils;
 
 
 @RunWith(MockitoJUnitRunner.class)
@@ -75,7 +76,7 @@ public class RootCAProviderTest {
 
         addField(provider, "caKeyPair", caKeyPair);
         addField(provider, "caCertificate", caCertificate);
-        addField(provider, "caKeyPair", caKeyPair);
+        addField(provider, "caCertificates", 
Collections.singletonList(caCertificate));
     }
 
     @After
@@ -129,6 +130,46 @@ public class RootCAProviderTest {
         
certificate.getClientCertificate().verify(caCertificate.getPublicKey());
     }
 
+    @Test
+    public void testGetCaCertificateWithChain() throws Exception {
+        final KeyPair rootKeyPair = CertUtils.generateRandomKeyPair(1024);
+        final X509Certificate rootCert = CertUtils.generateV3Certificate(null, 
rootKeyPair, rootKeyPair.getPublic(),
+                "CN=root", "SHA256withRSA", 365, null, null);
+        final KeyPair intermediateKeyPair = 
CertUtils.generateRandomKeyPair(1024);
+        final X509Certificate intermediateCert = 
CertUtils.generateV3Certificate(rootCert, rootKeyPair,
+                intermediateKeyPair.getPublic(), "CN=intermediate", 
"SHA256withRSA", 365, null, null);
+
+        final List<X509Certificate> chain = Arrays.asList(intermediateCert, 
rootCert);
+        addField(provider, "caKeyPair", intermediateKeyPair);
+        addField(provider, "caCertificate", intermediateCert);
+        addField(provider, "caCertificates", chain);
+
+        Assert.assertEquals(2, provider.getCaCertificate().size());
+        Assert.assertEquals(intermediateCert, 
provider.getCaCertificate().get(0));
+        Assert.assertEquals(rootCert, provider.getCaCertificate().get(1));
+    }
+
+    @Test
+    public void testIssueCertificateWithoutCsrAndChain() throws Exception {
+        final KeyPair rootKeyPair = CertUtils.generateRandomKeyPair(1024);
+        final X509Certificate rootCert = CertUtils.generateV3Certificate(null, 
rootKeyPair, rootKeyPair.getPublic(),
+                "CN=root", "SHA256withRSA", 365, null, null);
+        final KeyPair intermediateKeyPair = 
CertUtils.generateRandomKeyPair(1024);
+        final X509Certificate intermediateCert = 
CertUtils.generateV3Certificate(rootCert, rootKeyPair,
+                intermediateKeyPair.getPublic(), "CN=intermediate", 
"SHA256withRSA", 365, null, null);
+
+        addField(provider, "caKeyPair", intermediateKeyPair);
+        addField(provider, "caCertificate", intermediateCert);
+        addField(provider, "caCertificates", Arrays.asList(intermediateCert, 
rootCert));
+
+        final Certificate certificate = 
provider.issueCertificate(Arrays.asList("domain1.com"), null, 1);
+        Assert.assertNotNull(certificate);
+        Assert.assertEquals(2, certificate.getCaCertificates().size());
+        Assert.assertEquals(intermediateCert, 
certificate.getCaCertificates().get(0));
+        Assert.assertEquals(rootCert, certificate.getCaCertificates().get(1));
+        
certificate.getClientCertificate().verify(intermediateKeyPair.getPublic());
+    }
+
     @Test
     public void testRevokeCertificate() throws Exception {
         
Assert.assertTrue(provider.revokeCertificate(CertUtils.generateRandomBigInt(), 
"anyString"));
@@ -177,8 +218,8 @@ public class RootCAProviderTest {
     }
 
     @Test
-    public void testIsManagementCertificateNoMatch() {
-        ReflectionTestUtils.setField(provider, 
"managementCertificateCustomSAN", "cloudstack");
+    public void testIsManagementCertificateNoMatch() throws Exception {
+        addField(provider, "managementCertificateCustomSAN", "cloudstack");
         try {
             X509Certificate certificate = Mockito.mock(X509Certificate.class);
             List<List<?>> altNames = new ArrayList<>();
@@ -193,9 +234,9 @@ public class RootCAProviderTest {
     }
 
     @Test
-    public void testIsManagementCertificateMatch() {
+    public void testIsManagementCertificateMatch() throws Exception {
         String customSAN = "cloudstack";
-        ReflectionTestUtils.setField(provider, 
"managementCertificateCustomSAN", customSAN);
+        addField(provider, "managementCertificateCustomSAN", customSAN);
         try {
             X509Certificate certificate = Mockito.mock(X509Certificate.class);
             List<List<?>> altNames = new ArrayList<>();
@@ -208,4 +249,58 @@ public class RootCAProviderTest {
             Assert.fail(String.format("Exception occurred: %s", 
e.getMessage()));
         }
     }
+
+    @Test
+    public void testLoadRootCACertificateWithMismatchedCert() throws Exception 
{
+        KeyPair otherKeyPair = CertUtils.generateRandomKeyPair(1024);
+        X509Certificate mismatchedCert = CertUtils.generateV3Certificate(null, 
otherKeyPair, otherKeyPair.getPublic(), "CN=other", "SHA256withRSA", 365, null, 
null);
+        String mismatchedPem = CertUtils.x509CertificateToPem(mismatchedCert);
+
+        ConfigKey<String> mockCertKey = Mockito.mock(ConfigKey.class);
+        Mockito.when(mockCertKey.value()).thenReturn(mismatchedPem);
+        addField(provider, "rootCACertificate", mockCertKey);
+
+        addField(provider, "caCertificate", null);
+        addField(provider, "caCertificates", null);
+
+        Boolean result = provider.loadRootCACertificate();
+        Assert.assertFalse(result);
+        Assert.assertNull(provider.getCaCertificate());
+    }
+
+    @Test
+    public void testSaveNewRootCACertificateWithStaleCache() throws Exception {
+        ConfigurationDao configDao = Mockito.mock(ConfigurationDao.class);
+        addField(provider, "configDao", configDao);
+
+        ConfigKey<String> mockCertKey = Mockito.mock(ConfigKey.class);
+        
Mockito.when(mockCertKey.key()).thenReturn("ca.plugin.root.ca.certificate");
+        Mockito.when(mockCertKey.category()).thenReturn("Hidden");
+        addField(provider, "rootCACertificate", mockCertKey);
+
+        ConfigKey<String> mockIssuerKey = Mockito.mock(ConfigKey.class);
+        
Mockito.when(mockIssuerKey.value()).thenReturn("CN=ca.cloudstack.apache.org");
+        addField(provider, "rootCAIssuerDN", mockIssuerKey);
+
+        addField(provider, "caCertificate", null);
+        addField(provider, "caCertificates", null);
+
+        Mockito.when(configDao.update(Mockito.anyString(), 
Mockito.anyString(), Mockito.anyString())).thenReturn(true);
+
+        Boolean result = provider.saveNewRootCACertificate();
+        Assert.assertTrue(result);
+        Assert.assertNotNull(provider.getCaCertificate());
+        Assert.assertEquals(1, provider.getCaCertificate().size());
+    }
+
+    @Test
+    public void testValidateCACertificatePem() throws Exception {
+        String truncatedPem = "-----BEGIN 
CERTIFICATE-----\nMIICxTCCAa0CAQAw\n";
+        try {
+            RootCAProvider.validateCACertificatePem(truncatedPem);
+            Assert.fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException e) {
+            Assert.assertTrue(e.getMessage().contains("is not a valid PEM 
certificate"));
+        }
+    }
 }
diff --git 
a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDeliveryThread.java
 
b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDeliveryThread.java
index ac840c00be3..3f2d85458d3 100644
--- 
a/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDeliveryThread.java
+++ 
b/plugins/event-bus/webhook/src/main/java/org/apache/cloudstack/mom/webhook/WebhookDeliveryThread.java
@@ -48,6 +48,8 @@ import org.apache.http.HttpHeaders;
 import org.apache.http.client.config.RequestConfig;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpPost;
+import javax.net.ssl.SSLContext;
+
 import org.apache.http.conn.ssl.NoopHostnameVerifier;
 import org.apache.http.conn.ssl.TrustAllStrategy;
 import org.apache.http.entity.ContentType;
@@ -97,7 +99,9 @@ public class WebhookDeliveryThread implements Runnable {
 
     protected void setHttpClient() throws NoSuchAlgorithmException, 
KeyStoreException, KeyManagementException {
         if (webhook.isSslVerification()) {
-            httpClient = HttpClients.createDefault();
+            httpClient = HttpClients.custom()
+                    .setSSLContext(SSLContext.getDefault())
+                    .build();
             return;
         }
         httpClient = HttpClients
diff --git a/scripts/util/keystore-cert-import 
b/scripts/util/keystore-cert-import
index a9465f273a3..cf355e09845 100755
--- a/scripts/util/keystore-cert-import
+++ b/scripts/util/keystore-cert-import
@@ -70,8 +70,8 @@ elif [ ! -f "$CACERT_FILE" ]; then
 fi
 
 # Import cacerts into the keystore
-awk '/-----BEGIN CERTIFICATE-----?/{n++}{print > "cloudca." n }' "$CACERT_FILE"
-for caChain in $(ls cloudca.*); do
+awk 'BEGIN{n=0} /-----BEGIN CERTIFICATE-----/{n++} n>0{print > "cloudca." n }' 
"$CACERT_FILE"
+for caChain in $(ls cloudca.* 2>/dev/null); do
     keytool -delete -noprompt -alias "$caChain" -keystore "$KS_FILE" 
-storepass "$KS_PASS" > /dev/null 2>&1 || true
     keytool -import -noprompt -storepass "$KS_PASS" -trustcacerts -alias 
"$caChain" -file "$caChain" -keystore "$KS_FILE" > /dev/null 2>&1
 done
@@ -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{n=0} /-----BEGIN CERTIFICATE-----/{n++} n>0{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/server/src/main/java/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java
 
b/server/src/main/java/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java
index efda72555e2..b9fa3f0ebae 100644
--- 
a/server/src/main/java/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java
+++ 
b/server/src/main/java/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java
@@ -21,7 +21,6 @@ import static 
com.cloud.configuration.ConfigurationManagerImpl.ADD_HOST_ON_SERVI
 import java.net.InetAddress;
 import java.net.URI;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -32,11 +31,8 @@ import javax.naming.ConfigurationException;
 
 import org.apache.cloudstack.agent.lb.IndirectAgentLB;
 import org.apache.cloudstack.ca.CAManager;
-import org.apache.cloudstack.ca.SetupCertificateCommand;
 import org.apache.cloudstack.direct.download.DirectDownloadManager;
-import org.apache.cloudstack.framework.ca.Certificate;
 import org.apache.cloudstack.utils.cache.LazyCache;
-import org.apache.cloudstack.utils.security.KeyStoreUtils;
 
 import com.cloud.agent.AgentManager;
 import com.cloud.agent.Listener;
@@ -66,7 +62,6 @@ import com.cloud.resource.DiscovererBase;
 import com.cloud.resource.ResourceStateAdapter;
 import com.cloud.resource.ServerResource;
 import com.cloud.resource.UnableDeleteHostException;
-import com.cloud.utils.PasswordGenerator;
 import com.cloud.utils.StringUtils;
 import com.cloud.utils.UuidUtils;
 import com.cloud.utils.exception.CloudRuntimeException;
@@ -174,55 +169,7 @@ public abstract class LibvirtServerDiscoverer extends 
DiscovererBase implements
             throw new CloudRuntimeException("Cannot secure agent communication 
because SSH connection is invalid for host IP=" + agentIp);
         }
 
-        Integer validityPeriod = CAManager.CertValidityPeriod.value();
-        if (validityPeriod < 1) {
-            validityPeriod = 1;
-        }
-
-        String keystorePassword = PasswordGenerator.generateRandomPassword(16);
-        final SSHCmdHelper.SSHCmdResult keystoreSetupResult = 
SSHCmdHelper.sshExecuteCmdWithResult(sshConnection,
-                String.format("sudo 
/usr/share/cloudstack-common/scripts/util/%s " +
-                                "/etc/cloudstack/agent/agent.properties " +
-                                "/etc/cloudstack/agent/%s " +
-                                "%s %d " +
-                                "/etc/cloudstack/agent/%s",
-                        KeyStoreUtils.KS_SETUP_SCRIPT,
-                        KeyStoreUtils.KS_FILENAME,
-                        keystorePassword,
-                        validityPeriod,
-                        KeyStoreUtils.CSR_FILENAME));
-
-        if (!keystoreSetupResult.isSuccess()) {
-            throw new CloudRuntimeException("Failed to setup keystore on the 
KVM host: " + agentIp);
-        }
-
-        final Certificate certificate = 
caManager.issueCertificate(keystoreSetupResult.getStdOut(), 
Arrays.asList(agentHostname, agentIp), Collections.singletonList(agentIp), 
null, null);
-        if (certificate == null || certificate.getClientCertificate() == null) 
{
-            throw new CloudRuntimeException("Failed to issue certificates for 
KVM host agent: " + agentIp);
-        }
-
-        final SetupCertificateCommand certificateCommand = new 
SetupCertificateCommand(certificate);
-        final SSHCmdHelper.SSHCmdResult setupCertResult = 
SSHCmdHelper.sshExecuteCmdWithResult(sshConnection,
-                String.format("sudo 
/usr/share/cloudstack-common/scripts/util/%s " +
-                                "/etc/cloudstack/agent/agent.properties %s " +
-                                "/etc/cloudstack/agent/%s %s " +
-                                "/etc/cloudstack/agent/%s \"%s\" " +
-                                "/etc/cloudstack/agent/%s \"%s\" " +
-                                "/etc/cloudstack/agent/%s \"%s\"",
-                        KeyStoreUtils.KS_IMPORT_SCRIPT,
-                        keystorePassword,
-                        KeyStoreUtils.KS_FILENAME,
-                        KeyStoreUtils.SSH_MODE,
-                        KeyStoreUtils.CERT_FILENAME,
-                        certificateCommand.getEncodedCertificate(),
-                        KeyStoreUtils.CACERT_FILENAME,
-                        certificateCommand.getEncodedCaCertificates(),
-                        KeyStoreUtils.PKEY_FILENAME,
-                        certificateCommand.getEncodedPrivateKey()));
-
-        if (setupCertResult != null && !setupCertResult.isSuccess()) {
-            throw new CloudRuntimeException("Failed to setup certificate in 
the KVM agent's keystore file, please see logs and configure manually!");
-        }
+        caManager.provisionCertificateViaSsh(sshConnection, agentIp, 
agentHostname, null);
 
         if (logger.isDebugEnabled()) {
             logger.debug("Succeeded to import certificate in the keystore for 
agent on the KVM host: " + agentIp + ". Agent secured and trusted.");
diff --git a/server/src/main/java/org/apache/cloudstack/ca/CAManagerImpl.java 
b/server/src/main/java/org/apache/cloudstack/ca/CAManagerImpl.java
index 2b4e7ddc9d4..73ff79301fb 100644
--- a/server/src/main/java/org/apache/cloudstack/ca/CAManagerImpl.java
+++ b/server/src/main/java/org/apache/cloudstack/ca/CAManagerImpl.java
@@ -22,12 +22,14 @@ import java.math.BigInteger;
 import java.security.GeneralSecurityException;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
+import java.security.SecureRandom;
 import java.security.cert.CertificateExpiredException;
 import java.security.cert.CertificateNotYetValidException;
 import java.security.cert.CertificateParsingException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -39,6 +41,21 @@ import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLEngine;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import com.trilead.ssh2.Connection;
+import org.apache.cloudstack.api.ApiConstants;
+import 
org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
+import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
+import com.cloud.host.HostVO;
+import com.cloud.utils.PasswordGenerator;
+import com.cloud.utils.ssh.SSHCmdHelper;
+import com.cloud.vm.VMInstanceVO;
+import com.cloud.vm.dao.VMInstanceDao;
+import org.apache.cloudstack.utils.security.KeyStoreUtils;
+import org.apache.commons.lang3.math.NumberUtils;
 
 import org.apache.cloudstack.api.ApiErrorCode;
 import org.apache.cloudstack.api.ServerApiException;
@@ -60,6 +77,7 @@ import org.joda.time.DateTime;
 import org.joda.time.DateTimeZone;
 
 import com.cloud.agent.AgentManager;
+import com.cloud.agent.api.routing.NetworkElementCommand;
 import com.cloud.alert.AlertManager;
 import com.cloud.certificate.CrlVO;
 import com.cloud.certificate.dao.CrlDao;
@@ -81,6 +99,12 @@ public class CAManagerImpl extends ManagerBase implements 
CAManager {
     @Inject
     private HostDao hostDao;
     @Inject
+    private VMInstanceDao vmInstanceDao;
+    @Inject
+    private NetworkOrchestrationService networkOrchestrationService;
+    @Inject
+    private ConfigurationDao configDao;
+    @Inject
     private AgentManager agentManager;
     @Inject
     private BackgroundPollManager backgroundPollManager;
@@ -177,12 +201,17 @@ public class CAManagerImpl extends ManagerBase implements 
CAManager {
 
     @Override
     @ActionEvent(eventType = EventTypes.EVENT_CA_CERTIFICATE_PROVISION, 
eventDescription = "provisioning certificate for host", async = true)
-    public boolean provisionCertificate(final Host host, final Boolean 
reconnect, final String caProvider) {
+    public boolean provisionCertificate(final Host host, final Boolean 
reconnect, final String caProvider, final boolean forced) {
         if (host == null) {
             throw new CloudRuntimeException("Unable to find valid host to 
renew certificate for");
         }
         CallContext.current().setEventDetails("Host ID: " + host.getUuid());
         CallContext.current().putContextParameter(Host.class, host.getUuid());
+
+        if (forced) {
+            return provisionCertificateForced(host, reconnect, caProvider);
+        }
+
         String csr = null;
 
         try {
@@ -200,6 +229,141 @@ public class CAManagerImpl extends ManagerBase implements 
CAManager {
         }
     }
 
+    protected boolean provisionCertificateForced(Host host, Boolean reconnect, 
String caProvider) {
+        if (host.getType() == Host.Type.Routing && host.getHypervisorType() == 
com.cloud.hypervisor.Hypervisor.HypervisorType.KVM) {
+            return provisionKvmHostViaSsh(host, caProvider);
+        } else if (host.getType() == Host.Type.ConsoleProxy || host.getType() 
== Host.Type.SecondaryStorageVM) {
+            return provisionSystemVmViaSsh(host, reconnect, caProvider);
+        }
+        throw new CloudRuntimeException("Forced certificate provisioning is 
only supported for KVM hosts and SystemVMs.");
+    }
+
+    @Override
+    public void provisionCertificateViaSsh(final Connection sshConnection, 
final String agentIp, final String agentHostname, final String caProvider) {
+        Integer validityPeriod = CAManager.CertValidityPeriod.value();
+        if (validityPeriod < 1) {
+            validityPeriod = 1;
+        }
+
+        String keystorePassword = PasswordGenerator.generateRandomPassword(16);
+
+        // 1. Setup Keystore and Generate CSR
+        final SSHCmdHelper.SSHCmdResult keystoreSetupResult = 
SSHCmdHelper.sshExecuteCmdWithResult(sshConnection,
+                String.format("sudo 
/usr/share/cloudstack-common/scripts/util/%s " +
+                              "/etc/cloudstack/agent/agent.properties " +
+                              "/etc/cloudstack/agent/%s " +
+                              "%s %d " +
+                              "/etc/cloudstack/agent/%s",
+                        KeyStoreUtils.KS_SETUP_SCRIPT,
+                        KeyStoreUtils.KS_FILENAME,
+                        keystorePassword,
+                        validityPeriod,
+                        KeyStoreUtils.CSR_FILENAME));
+
+        if (!keystoreSetupResult.isSuccess()) {
+            throw new CloudRuntimeException("Failed to setup keystore and 
generate CSR via SSH on host: " + agentIp);
+        }
+
+        // 2. Issue Certificate based on returned CSR
+        final String csr = keystoreSetupResult.getStdOut();
+        final Certificate certificate = issueCertificate(csr, 
Arrays.asList(agentHostname, agentIp),
+                Collections.singletonList(agentIp), null, caProvider);
+
+        if (certificate == null || certificate.getClientCertificate() == null) 
{
+            throw new CloudRuntimeException("Failed to issue certificates for 
host: " + agentIp);
+        }
+
+        // 3. Import Certificate into agent keystore
+        final SetupCertificateCommand certificateCommand = new 
SetupCertificateCommand(certificate);
+        final SSHCmdHelper.SSHCmdResult setupCertResult = 
SSHCmdHelper.sshExecuteCmdWithResult(sshConnection,
+                String.format("sudo 
/usr/share/cloudstack-common/scripts/util/%s " +
+                              "/etc/cloudstack/agent/agent.properties %s " +
+                              "/etc/cloudstack/agent/%s %s " +
+                              "/etc/cloudstack/agent/%s \"%s\" " +
+                              "/etc/cloudstack/agent/%s \"%s\" " +
+                              "/etc/cloudstack/agent/%s \"%s\"",
+                        KeyStoreUtils.KS_IMPORT_SCRIPT,
+                        keystorePassword,
+                        KeyStoreUtils.KS_FILENAME,
+                        KeyStoreUtils.SSH_MODE,
+                        KeyStoreUtils.CERT_FILENAME,
+                        certificateCommand.getEncodedCertificate(),
+                        KeyStoreUtils.CACERT_FILENAME,
+                        certificateCommand.getEncodedCaCertificates(),
+                        KeyStoreUtils.PKEY_FILENAME,
+                        certificateCommand.getEncodedPrivateKey()));
+
+        if (!setupCertResult.isSuccess()) {
+            throw new CloudRuntimeException("Failed to import certificates 
into agent keystore via SSH on host: " + agentIp);
+        }
+    }
+
+    private boolean provisionKvmHostViaSsh(Host host, String caProvider) {
+        final HostVO hostVO = (HostVO) host;
+        hostDao.loadDetails(hostVO);
+        String username = hostVO.getDetail(ApiConstants.USERNAME);
+        String password = hostVO.getDetail(ApiConstants.PASSWORD);
+        String hostIp = host.getPrivateIpAddress();
+
+        int port = 
AgentManager.KVMHostDiscoverySshPort.valueIn(host.getClusterId());
+        if (hostVO.getDetail(Host.HOST_SSH_PORT) != null) {
+            port = NumberUtils.toInt(hostVO.getDetail(Host.HOST_SSH_PORT), 
port);
+        }
+
+        Connection sshConnection = null;
+        try {
+            sshConnection = new Connection(hostIp, port);
+            sshConnection.connect(null, 60000, 60000);
+
+            String privateKey = configDao.getValue("ssh.privatekey");
+            if 
(!SSHCmdHelper.acquireAuthorizedConnectionWithPublicKey(sshConnection, 
username, privateKey)) {
+                if (StringUtils.isEmpty(password) || 
!sshConnection.authenticateWithPassword(username, password)) {
+                    throw new CloudRuntimeException("Failed to authenticate to 
host via SSH for forced provisioning: " + hostIp);
+                }
+            }
+
+            provisionCertificateViaSsh(sshConnection, hostIp, host.getName(), 
caProvider);
+
+            String sudoPrefix = "root".equals(username) ? "" : "sudo ";
+            SSHCmdHelper.sshExecuteCmd(sshConnection, sudoPrefix + "systemctl 
restart libvirtd");
+            SSHCmdHelper.sshExecuteCmd(sshConnection, sudoPrefix + "systemctl 
restart cloudstack-agent");
+
+            return true;
+        } catch (Exception e) {
+            logger.error("Error during forced SSH provisioning for KVM host " 
+ host.getUuid(), e);
+            return false;
+        } finally {
+            if (sshConnection != null) {
+                sshConnection.close();
+            }
+        }
+    }
+
+    private boolean provisionSystemVmViaSsh(Host host, Boolean reconnect, 
String caProvider) {
+        VMInstanceVO vm = vmInstanceDao.findVMByInstanceName(host.getName());
+        if (vm == null) {
+            throw new CloudRuntimeException("Cannot find underlying VM for 
host: " + host.getName());
+        }
+
+        final Map<String, String> sshAccessDetails = 
networkOrchestrationService.getSystemVMAccessDetails(vm);
+        final Map<String, String> ipAddressDetails = new 
HashMap<>(sshAccessDetails);
+        ipAddressDetails.remove(NetworkElementCommand.ROUTER_NAME);
+
+        try {
+            final Host hypervisorHost = hostDao.findById(vm.getHostId());
+            if (hypervisorHost == null) {
+                throw new CloudRuntimeException("Cannot find hypervisor host 
for system VM: " + host.getName());
+            }
+
+            final Certificate certificate = issueCertificate(null, 
Arrays.asList(vm.getHostName(), vm.getInstanceName()),
+                    new ArrayList<>(ipAddressDetails.values()), 
CertValidityPeriod.value(), caProvider);
+            return deployCertificate(hypervisorHost, certificate, reconnect, 
sshAccessDetails);
+        } catch (Exception e) {
+            logger.error("Failed to provision system VM " + host.getName() + " 
via hypervisor SSH proxy. Ensure the hypervisor host is connected.", e);
+            return false;
+        }
+    }
+
     @Override
     public String generateKeyStoreAndCsr(final Host host, final Map<String, 
String> sshAccessDetails) throws AgentUnavailableException, 
OperationTimedoutException {
         final SetupKeyStoreCommand cmd = new 
SetupKeyStoreCommand(CertValidityPeriod.value());
@@ -211,11 +375,6 @@ public class CAManagerImpl extends ManagerBase implements 
CAManager {
         return answer.getCsr();
     }
 
-    private boolean isValidSystemVMType(Host.Type type) {
-        return Host.Type.SecondaryStorageVM.equals(type) ||
-                Host.Type.ConsoleProxy.equals(type);
-    }
-
     @Override
     public boolean deployCertificate(final Host host, final Certificate 
certificate, final Boolean reconnect, final Map<String, String> 
sshAccessDetails)
             throws AgentUnavailableException, OperationTimedoutException {
@@ -340,7 +499,7 @@ public class CAManagerImpl extends ManagerBase implements 
CAManager {
                         if (AutomaticCertRenewal.valueIn(host.getClusterId())) 
{
                             try {
                                 logger.debug("Attempting certificate 
auto-renewal for " + hostDescription, e);
-                                boolean result = 
caManager.provisionCertificate(host, false, null);
+                                boolean result = 
caManager.provisionCertificate(host, false, null, false);
                                 if (result) {
                                     logger.debug("Succeeded in auto-renewing 
certificate for " + hostDescription, e);
                                 } else {
@@ -400,9 +559,57 @@ public class CAManagerImpl extends ManagerBase implements 
CAManager {
             logger.error("Failed to find valid configured CA provider, please 
check!");
             return false;
         }
+        if (CaInjectDefaultTruststore.value()) {
+            injectCaCertIntoDefaultTruststore();
+        }
         return true;
     }
 
+    private void injectCaCertIntoDefaultTruststore() {
+        try {
+            final List<X509Certificate> caCerts = 
configuredCaProvider.getCaCertificate();
+            if (caCerts == null || caCerts.isEmpty()) {
+                logger.debug("No CA certificates found from the configured 
provider, skipping JVM truststore injection");
+                return;
+            }
+
+            final KeyStore trustStore = 
KeyStore.getInstance(KeyStore.getDefaultType());
+            trustStore.load(null, null);
+
+            // Copy existing default trusted certs
+            final TrustManagerFactory defaultTmf = 
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+            defaultTmf.init((KeyStore) null);
+            int aliasIndex = 0;
+            for (final TrustManager tm : defaultTmf.getTrustManagers()) {
+                if (tm instanceof X509TrustManager) {
+                    for (final X509Certificate cert : ((X509TrustManager) 
tm).getAcceptedIssuers()) {
+                        trustStore.setCertificateEntry("default-ca-" + 
aliasIndex++, cert);
+                    }
+                }
+            }
+
+            // Add CA provider's certificates
+            int count = 0;
+            for (final X509Certificate caCert : caCerts) {
+                final String alias = "cloudstack-ca-" + count;
+                trustStore.setCertificateEntry(alias, caCert);
+                count++;
+                logger.info("Injected CA certificate into JVM default 
truststore: subject={}, alias={}",
+                    caCert.getSubjectX500Principal().getName(), alias);
+            }
+
+            // Reinitialize default SSLContext with the updated truststore
+            final TrustManagerFactory updatedTmf = 
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+            updatedTmf.init(trustStore);
+            final SSLContext sslContext = SSLContext.getInstance("TLS");
+            sslContext.init(null, updatedTmf.getTrustManagers(), new 
SecureRandom());
+            SSLContext.setDefault(sslContext);
+            logger.info("Successfully injected {} CA certificate(s) into JVM 
default truststore", count);
+        } catch (final GeneralSecurityException | IOException e) {
+            logger.error("Failed to inject CA certificate into JVM default 
truststore", e);
+        }
+    }
+
     @Override
     public boolean configure(final String name, final Map<String, Object> 
params) throws ConfigurationException {
         backgroundPollManager.submitTask(new CABackgroundTask(this, hostDao));
@@ -433,7 +640,7 @@ public class CAManagerImpl extends ManagerBase implements 
CAManager {
     public ConfigKey<?>[] getConfigKeys() {
         return new ConfigKey<?>[] {CAProviderPlugin, CertKeySize, 
CertSignatureAlgorithm, CertValidityPeriod,
                 AutomaticCertRenewal, AllowHostIPInSysVMAgentCert, 
CABackgroundJobDelay, CertExpiryAlertPeriod,
-                CertManagementCustomSubjectAlternativeName
+                CertManagementCustomSubjectAlternativeName, 
CaInjectDefaultTruststore
         };
     }
 
diff --git 
a/server/src/test/java/org/apache/cloudstack/ca/CABackgroundTaskTest.java 
b/server/src/test/java/org/apache/cloudstack/ca/CABackgroundTaskTest.java
index 691bd882c07..7717e642766 100644
--- a/server/src/test/java/org/apache/cloudstack/ca/CABackgroundTaskTest.java
+++ b/server/src/test/java/org/apache/cloudstack/ca/CABackgroundTaskTest.java
@@ -115,19 +115,19 @@ public class CABackgroundTaskTest {
         certMap.put(hostIp, expiredCertificate);
         Assume.assumeThat(certMap.size() == 1, is(true));
         task.runInContext();
-        Mockito.verify(caManager, Mockito.times(1)).provisionCertificate(host, 
false, null);
+        Mockito.verify(caManager, Mockito.times(1)).provisionCertificate(host, 
false, null, false);
         Mockito.verify(caManager, 
Mockito.times(0)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), 
Mockito.anyString());
     }
 
     @Test
     public void testAutoRenewalEnabledWithExceptionsOnProvisioning() throws 
Exception {
         overrideDefaultConfigValue(AutomaticCertRenewal, "_defaultValue", 
"true");
-        Mockito.when(caManager.provisionCertificate(any(Host.class), 
anyBoolean(), nullable(String.class))).thenThrow(new 
CloudRuntimeException("some error"));
+        Mockito.when(caManager.provisionCertificate(any(Host.class), 
anyBoolean(), nullable(String.class), anyBoolean())).thenThrow(new 
CloudRuntimeException("some error"));
         
host.setManagementServerId(ManagementServerNode.getManagementServerId());
         certMap.put(hostIp, expiredCertificate);
         Assume.assumeThat(certMap.size() == 1, is(true));
         task.runInContext();
-        Mockito.verify(caManager, Mockito.times(1)).provisionCertificate(host, 
false, null);
+        Mockito.verify(caManager, Mockito.times(1)).provisionCertificate(host, 
false, null, false);
         Mockito.verify(caManager, 
Mockito.times(1)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), 
Mockito.anyString());
     }
 
@@ -138,12 +138,12 @@ public class CABackgroundTaskTest {
         Assume.assumeThat(certMap.size() == 1, is(true));
         // First round
         task.runInContext();
-        Mockito.verify(caManager, 
Mockito.times(0)).provisionCertificate(Mockito.any(Host.class), anyBoolean(), 
Mockito.anyString());
+        Mockito.verify(caManager, 
Mockito.times(0)).provisionCertificate(Mockito.any(Host.class), anyBoolean(), 
Mockito.anyString(), Mockito.anyBoolean());
         Mockito.verify(caManager, 
Mockito.times(1)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), 
Mockito.anyString());
         Mockito.reset(caManager);
         // Second round
         task.runInContext();
-        Mockito.verify(caManager, 
Mockito.times(0)).provisionCertificate(Mockito.any(Host.class), anyBoolean(), 
Mockito.anyString());
+        Mockito.verify(caManager, 
Mockito.times(0)).provisionCertificate(Mockito.any(Host.class), anyBoolean(), 
Mockito.anyString(), Mockito.anyBoolean());
         Mockito.verify(caManager, 
Mockito.times(0)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), 
Mockito.anyString());
     }
 
diff --git 
a/server/src/test/java/org/apache/cloudstack/ca/CAManagerImplTest.java 
b/server/src/test/java/org/apache/cloudstack/ca/CAManagerImplTest.java
index 08fa5529996..2d60833d35c 100644
--- a/server/src/test/java/org/apache/cloudstack/ca/CAManagerImplTest.java
+++ b/server/src/test/java/org/apache/cloudstack/ca/CAManagerImplTest.java
@@ -24,6 +24,7 @@ import com.cloud.certificate.CrlVO;
 import com.cloud.certificate.dao.CrlDao;
 import com.cloud.host.Host;
 import com.cloud.host.dao.HostDao;
+import com.cloud.utils.exception.CloudRuntimeException;
 import org.apache.cloudstack.api.ServerApiException;
 import org.apache.cloudstack.framework.ca.CAProvider;
 import org.apache.cloudstack.framework.ca.Certificate;
@@ -33,16 +34,34 @@ import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Mockito;
+import org.mockito.Spy;
 import org.mockito.junit.MockitoJUnitRunner;
 
 import java.lang.reflect.Field;
+import java.lang.reflect.Method;
 import java.math.BigInteger;
 import java.security.KeyPair;
 import java.security.cert.X509Certificate;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+
+
+import org.mockito.MockedStatic;
+import org.mockito.MockedConstruction;
+import com.cloud.utils.ssh.SSHCmdHelper;
+import com.cloud.host.HostVO;
+import com.cloud.vm.VMInstanceVO;
+import 
org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
+import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
+import com.cloud.vm.dao.VMInstanceDao;
+import com.trilead.ssh2.Connection;
+import com.cloud.agent.api.routing.NetworkElementCommand;
+import org.apache.cloudstack.api.ApiConstants;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -62,8 +81,15 @@ public class CAManagerImplTest {
     private AgentManager agentManager;
     @Mock
     private CAProvider caProvider;
-
-    private CAManagerImpl caManager;
+    @Mock
+    private VMInstanceDao vmInstanceDao;
+    @Mock
+    private NetworkOrchestrationService networkOrchestrationService;
+    @Mock
+    private ConfigurationDao configDao;
+    @InjectMocks
+    @Spy
+    private CAManagerImpl caManager = new CAManagerImpl();
 
     private void addField(final CAManagerImpl provider, final String name, 
final Object o) throws IllegalAccessException, NoSuchFieldException {
         Field f = CAManagerImpl.class.getDeclaredField(name);
@@ -73,10 +99,6 @@ public class CAManagerImplTest {
 
     @Before
     public void setUp() throws Exception {
-        caManager = new CAManagerImpl();
-        addField(caManager, "crlDao", crlDao);
-        addField(caManager, "hostDao", hostDao);
-        addField(caManager, "agentManager", agentManager);
         addField(caManager, "configuredCaProvider", caProvider);
 
         Mockito.when(caProvider.getProviderName()).thenReturn("root");
@@ -91,19 +113,19 @@ public class CAManagerImplTest {
     }
 
     @Test(expected = ServerApiException.class)
-    public void testIssueCertificateThrowsException() throws Exception {
+    public void testIssueCertificateThrowsException() {
         caManager.issueCertificate(null, null, null, 1, null);
     }
 
     @Test
-    public void testIssueCertificate() throws Exception {
+    public void testIssueCertificate() {
         caManager.issueCertificate(null, 
Collections.singletonList("domain.example"), null, 1, null);
         Mockito.verify(caProvider, 
Mockito.times(1)).issueCertificate(anyList(), nullable(List.class), anyInt());
         Mockito.verify(caProvider, 
Mockito.times(0)).issueCertificate(anyString(), anyList(), anyList(), anyInt());
     }
 
     @Test
-    public void testRevokeCertificate() throws Exception {
+    public void testRevokeCertificate() {
         final CrlVO crl = new CrlVO(CertUtils.generateRandomBigInt(), 
"some.domain", "some-uuid");
         Mockito.when(crlDao.revokeCertificate(Mockito.any(BigInteger.class), 
anyString())).thenReturn(crl);
         
Mockito.when(caProvider.revokeCertificate(Mockito.any(BigInteger.class), 
anyString())).thenReturn(true);
@@ -121,9 +143,190 @@ public class CAManagerImplTest {
         Mockito.when(agentManager.send(anyLong(), 
any(SetupCertificateCommand.class))).thenReturn(new 
SetupCertificateAnswer(true));
         Mockito.when(agentManager.send(anyLong(), 
any(SetupKeyStoreCommand.class))).thenReturn(new 
SetupKeystoreAnswer("someCsr"));
         Mockito.doNothing().when(agentManager).reconnect(Mockito.anyLong());
-        Assert.assertTrue(caManager.provisionCertificate(host, true, null));
+        Assert.assertTrue(caManager.provisionCertificate(host, true, null, 
false));
         Mockito.verify(agentManager, Mockito.times(1)).send(Mockito.anyLong(), 
any(SetupKeyStoreCommand.class));
         Mockito.verify(agentManager, Mockito.times(1)).send(Mockito.anyLong(), 
any(SetupCertificateCommand.class));
         Mockito.verify(agentManager, 
Mockito.times(1)).reconnect(Mockito.anyLong());
     }
+
+
+    @Test
+    public void testProvisionCertificateForced() throws Exception {
+        final Host host = Mockito.mock(Host.class);
+        
Mockito.doReturn(true).when(caManager).provisionCertificateForced(host, true, 
null);
+        Assert.assertTrue(caManager.provisionCertificate(host, true, null, 
true));
+        Mockito.verify(caManager, 
Mockito.times(1)).provisionCertificateForced(host, true, null);
+        Mockito.verify(agentManager, Mockito.never()).send(Mockito.anyLong(), 
any(SetupKeyStoreCommand.class));
+        Mockito.verify(agentManager, Mockito.never()).send(Mockito.anyLong(), 
any(SetupCertificateCommand.class));
+    }
+
+    @Test
+    public void testIssueCertificateWithCsr() throws Exception {
+        final KeyPair keyPair = CertUtils.generateRandomKeyPair(1024);
+        final X509Certificate x509 = CertUtils.generateV3Certificate(null, 
keyPair, keyPair.getPublic(), "CN=ca", "SHA256withRSA", 365, null, null);
+        Mockito.when(caProvider.issueCertificate(anyString(), anyList(), 
anyList(), anyInt()))
+                .thenReturn(new Certificate(x509, null, 
Collections.singletonList(x509)));
+        final Certificate result = caManager.issueCertificate("someCsr", 
Collections.singletonList("domain.example"), 
Collections.singletonList("1.2.3.4"), 365, null);
+        Assert.assertNotNull(result);
+        Mockito.verify(caProvider, 
Mockito.times(1)).issueCertificate(anyString(), anyList(), anyList(), anyInt());
+        Mockito.verify(caProvider, 
Mockito.never()).issueCertificate(anyList(), nullable(List.class), anyInt());
+    }
+
+    @Test(expected = CloudRuntimeException.class)
+    public void testProvisionCertificateNullHost() {
+        caManager.provisionCertificate(null, true, null, false);
+    }
+
+    @Test
+    public void testProvisionCertificateForSystemVm() throws Exception {
+        final Host host = Mockito.mock(Host.class);
+        Mockito.when(host.getType()).thenReturn(Host.Type.ConsoleProxy);
+        Mockito.when(host.getPrivateIpAddress()).thenReturn("1.2.3.4");
+        final KeyPair keyPair = CertUtils.generateRandomKeyPair(1024);
+        final X509Certificate x509 = CertUtils.generateV3Certificate(null, 
keyPair, keyPair.getPublic(), "CN=ca", "SHA256withRSA", 365, null, null);
+        Mockito.when(caProvider.issueCertificate(anyList(), anyList(), 
anyInt()))
+                .thenReturn(new Certificate(x509, null, 
Collections.singletonList(x509)));
+        Mockito.when(agentManager.send(anyLong(), 
any(SetupCertificateCommand.class))).thenReturn(new 
SetupCertificateAnswer(true));
+        Assert.assertTrue(caManager.provisionCertificate(host, false, null, 
false));
+        Mockito.verify(agentManager, Mockito.never()).send(Mockito.anyLong(), 
any(SetupKeyStoreCommand.class));
+        Mockito.verify(agentManager, Mockito.times(1)).send(Mockito.anyLong(), 
any(SetupCertificateCommand.class));
+        Mockito.verify(agentManager, 
Mockito.never()).reconnect(Mockito.anyLong());
+    }
+
+    @Test
+    public void testProvisionCertificateWithoutReconnect() throws Exception {
+        final Host host = Mockito.mock(Host.class);
+        Mockito.when(host.getPrivateIpAddress()).thenReturn("1.2.3.4");
+        final KeyPair keyPair = CertUtils.generateRandomKeyPair(1024);
+        final X509Certificate x509 = CertUtils.generateV3Certificate(null, 
keyPair, keyPair.getPublic(), "CN=ca", "SHA256withRSA", 365, null, null);
+        Mockito.when(caProvider.issueCertificate(anyString(), anyList(), 
anyList(), anyInt()))
+                .thenReturn(new Certificate(x509, null, 
Collections.singletonList(x509)));
+        Mockito.when(agentManager.send(anyLong(), 
any(SetupCertificateCommand.class))).thenReturn(new 
SetupCertificateAnswer(true));
+        Mockito.when(agentManager.send(anyLong(), 
any(SetupKeyStoreCommand.class))).thenReturn(new 
SetupKeystoreAnswer("someCsr"));
+        Assert.assertTrue(caManager.provisionCertificate(host, false, null, 
false));
+        Mockito.verify(agentManager, 
Mockito.never()).reconnect(Mockito.anyLong());
+    }
+
+    @Test
+    public void testRevokeCertificateReturnsFalseWhenCrlIsNull() {
+        Mockito.when(crlDao.revokeCertificate(Mockito.any(BigInteger.class), 
anyString())).thenReturn(null);
+        Assert.assertFalse(caManager.revokeCertificate(BigInteger.ONE, 
"some.domain", null));
+        Mockito.verify(caProvider, 
Mockito.never()).revokeCertificate(Mockito.any(BigInteger.class), anyString());
+    }
+
+    @Test
+    public void testRevokeCertificateReturnsFalseWhenSerialMismatch() {
+        final CrlVO crl = new CrlVO(BigInteger.ONE, "some.domain", 
"some-uuid");
+        Mockito.when(crlDao.revokeCertificate(Mockito.any(BigInteger.class), 
anyString())).thenReturn(crl);
+        Assert.assertFalse(caManager.revokeCertificate(BigInteger.TWO, 
"some.domain", null));
+        Mockito.verify(caProvider, 
Mockito.never()).revokeCertificate(Mockito.any(BigInteger.class), anyString());
+    }
+
+    @Test
+    public void testPurgeHostCertificate() throws Exception {
+        final Host host = Mockito.mock(Host.class);
+        Mockito.when(host.getPrivateIpAddress()).thenReturn("10.0.0.1");
+        Mockito.when(host.getPublicIpAddress()).thenReturn("192.168.0.1");
+        final KeyPair keyPair = CertUtils.generateRandomKeyPair(1024);
+        final X509Certificate x509 = CertUtils.generateV3Certificate(null, 
keyPair,
+                keyPair.getPublic(), "CN=ca", "SHA256withRSA",
+                365, null, null);
+        caManager.getActiveCertificatesMap().put("10.0.0.1", x509);
+        caManager.getActiveCertificatesMap().put("192.168.0.1", x509);
+        caManager.purgeHostCertificate(host);
+        
Assert.assertFalse(caManager.getActiveCertificatesMap().containsKey("10.0.0.1"));
+        
Assert.assertFalse(caManager.getActiveCertificatesMap().containsKey("192.168.0.1"));
+    }
+    @Test
+    public void testProvisionCertificateViaSsh() throws Exception {
+        Connection sshConnection = Mockito.mock(Connection.class);
+        final String agentIp = "192.168.1.1";
+        final String agentHostname = "host1";
+        final String caProviderStr = "root";
+
+        try (MockedStatic<SSHCmdHelper> sshCmdHelperMock = 
Mockito.mockStatic(SSHCmdHelper.class)) {
+            SSHCmdHelper.SSHCmdResult successResult = new 
SSHCmdHelper.SSHCmdResult(0, "someCsr", "");
+            sshCmdHelperMock.when(() -> 
SSHCmdHelper.sshExecuteCmdWithResult(Mockito.eq(sshConnection), 
Mockito.anyString()))
+                    .thenReturn(successResult);
+
+            final KeyPair keyPair = CertUtils.generateRandomKeyPair(1024);
+            final X509Certificate x509 = CertUtils.generateV3Certificate(null, 
keyPair, keyPair.getPublic(), "CN=ca", "SHA256withRSA", 365, null, null);
+            Mockito.doReturn(new Certificate(x509, null, 
Collections.singletonList(x509)))
+                   .when(caManager).issueCertificate(Mockito.anyString(), 
Mockito.anyList(), Mockito.anyList(), Mockito.nullable(Integer.class), 
Mockito.anyString());
+
+            caManager.provisionCertificateViaSsh(sshConnection, agentIp, 
agentHostname, caProviderStr);
+
+            sshCmdHelperMock.verify(() -> 
SSHCmdHelper.sshExecuteCmdWithResult(Mockito.eq(sshConnection), 
Mockito.contains("keystore-setup")), Mockito.times(1));
+            sshCmdHelperMock.verify(() -> 
SSHCmdHelper.sshExecuteCmdWithResult(Mockito.eq(sshConnection), 
Mockito.contains("keystore-cert-import")), Mockito.times(1));
+        }
+    }
+
+    @Test
+    public void testProvisionKvmHostViaSsh() throws Exception {
+        HostVO host = Mockito.mock(HostVO.class);
+        Mockito.when(host.getPrivateIpAddress()).thenReturn("192.168.1.1");
+        Mockito.when(host.getName()).thenReturn("host1");
+        Mockito.when(host.getClusterId()).thenReturn(1L);
+
+        Mockito.doNothing().when(hostDao).loadDetails(host);
+        Mockito.when(host.getDetail(ApiConstants.USERNAME)).thenReturn("root");
+        
Mockito.when(host.getDetail(ApiConstants.PASSWORD)).thenReturn("password");
+
+        
Mockito.when(configDao.getValue("ssh.privatekey")).thenReturn("privatekey");
+
+        try (MockedConstruction<Connection> ignored = 
Mockito.mockConstruction(Connection.class,
+                (mock, context) -> {
+                    // Do nothing on connect
+                });
+             MockedStatic<SSHCmdHelper> sshCmdHelperMock = 
Mockito.mockStatic(SSHCmdHelper.class)) {
+            sshCmdHelperMock.when(() -> 
SSHCmdHelper.acquireAuthorizedConnectionWithPublicKey(Mockito.any(Connection.class),
 Mockito.anyString(), Mockito.anyString()))
+                    .thenReturn(true);
+
+            
Mockito.doNothing().when(caManager).provisionCertificateViaSsh(Mockito.any(Connection.class),
 Mockito.anyString(), Mockito.anyString(), Mockito.anyString());
+
+            Method method = 
CAManagerImpl.class.getDeclaredMethod("provisionKvmHostViaSsh", Host.class, 
String.class);
+            method.setAccessible(true);
+            boolean result = (Boolean) method.invoke(caManager, host, "root");
+
+            Assert.assertTrue(result);
+            Mockito.verify(caManager, 
Mockito.times(1)).provisionCertificateViaSsh(Mockito.any(Connection.class), 
Mockito.eq("192.168.1.1"), Mockito.eq("host1"), Mockito.eq("root"));
+            sshCmdHelperMock.verify(() -> 
SSHCmdHelper.sshExecuteCmd(Mockito.any(Connection.class), Mockito.eq("systemctl 
restart libvirtd")), Mockito.times(1));
+            sshCmdHelperMock.verify(() -> 
SSHCmdHelper.sshExecuteCmd(Mockito.any(Connection.class), Mockito.eq("systemctl 
restart cloudstack-agent")), Mockito.times(1));
+        }
+    }
+
+    @Test
+    public void testProvisionSystemVmViaSsh() throws Exception {
+        Host host = Mockito.mock(Host.class);
+        Mockito.when(host.getName()).thenReturn("v-1-VM");
+
+        VMInstanceVO vm = Mockito.mock(VMInstanceVO.class);
+        Mockito.when(vm.getHostId()).thenReturn(1L);
+        Mockito.when(vm.getHostName()).thenReturn("host1");
+        Mockito.when(vm.getInstanceName()).thenReturn("v-1-VM");
+        
Mockito.when(vmInstanceDao.findVMByInstanceName("v-1-VM")).thenReturn(vm);
+
+        Map<String, String> accessDetails = new HashMap<>();
+        accessDetails.put(NetworkElementCommand.ROUTER_IP, "192.168.1.2");
+        
Mockito.when(networkOrchestrationService.getSystemVMAccessDetails(vm)).thenReturn(accessDetails);
+
+        HostVO hypervisorHost = Mockito.mock(HostVO.class);
+        Mockito.when(hostDao.findById(1L)).thenReturn(hypervisorHost);
+
+        final KeyPair keyPair = CertUtils.generateRandomKeyPair(1024);
+        final X509Certificate x509 = CertUtils.generateV3Certificate(null, 
keyPair, keyPair.getPublic(), "CN=ca", "SHA256withRSA", 365, null, null);
+        Certificate cert = new Certificate(x509, null, 
Collections.singletonList(x509));
+        Mockito.doReturn(cert)
+               
.when(caManager).issueCertificate(Mockito.nullable(String.class), 
Mockito.anyList(), Mockito.anyList(), Mockito.nullable(Integer.class), 
Mockito.anyString());
+
+        Mockito.doReturn(true)
+               .when(caManager).deployCertificate(Mockito.eq(hypervisorHost), 
Mockito.eq(cert), Mockito.anyBoolean(), Mockito.eq(accessDetails));
+
+        Method method = 
CAManagerImpl.class.getDeclaredMethod("provisionSystemVmViaSsh", Host.class, 
Boolean.class, String.class);
+        method.setAccessible(true);
+        boolean result = (Boolean) method.invoke(caManager, host, true, 
"root");
+
+        Assert.assertTrue(result);
+        Mockito.verify(caManager, 
Mockito.times(1)).deployCertificate(Mockito.eq(hypervisorHost), 
Mockito.eq(cert), Mockito.eq(true), Mockito.eq(accessDetails));
+    }
 }
diff --git a/systemvm/patch-sysvms.sh b/systemvm/patch-sysvms.sh
index 88d720e0f32..8d96de9ba3b 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{n=0} /-----BEGIN CERTIFICATE-----/{n++} n>0{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/test/integration/smoke/test_certauthority_root.py 
b/test/integration/smoke/test_certauthority_root.py
index dc6420d6369..491b8abeb2e 100644
--- a/test/integration/smoke/test_certauthority_root.py
+++ b/test/integration/smoke/test_certauthority_root.py
@@ -15,9 +15,12 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import re
+from datetime import datetime, timedelta
+
 from nose.plugins.attrib import attr
 from marvin.cloudstackTestCase import cloudstackTestCase
-from marvin.lib.utils import cleanup_resources
+from marvin.lib.utils import cleanup_resources, wait_until
 from marvin.lib.base import *
 from marvin.lib.common import list_hosts
 
@@ -60,6 +63,29 @@ class TestCARootProvider(cloudstackTestCase):
         except Exception as e:
             print(f"Certificate verification failed: {e}")
 
+
+    def parseCertificateChain(self, pem):
+        """Split a PEM blob containing one or more certificates into a list of 
x509 objects."""
+        certs = []
+        matches = re.findall(
+            r'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----',
+            pem,
+            re.DOTALL
+        )
+        for match in matches:
+            certs.append(x509.load_pem_x509_certificate(match.encode(), 
default_backend()))
+        return certs
+
+
+    def assertSignatureValid(self, issuerCert, cert):
+        """Verify cert is signed by issuerCert; raise on failure."""
+        issuerCert.public_key().verify(
+            cert.signature,
+            cert.tbs_certificate_bytes,
+            padding.PKCS1v15(),
+            cert.signature_hash_algorithm,
+        )
+
     def setUp(self):
         self.apiclient = self.testClient.getApiClient()
         self.dbclient = self.testClient.getDbConnection()
@@ -224,3 +250,152 @@ class TestCARootProvider(cloudstackTestCase):
                 self.assertTrue(len(hosts) == 1)
             else:
                 self.fail("Failed to have systemvm host in Up state after cert 
provisioning")
+
+
+    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], 
required_hardware=False)
+    def test_ca_certificate_chain_validity(self):
+        """
+            Tests that listCaCertificate returns a valid certificate chain.
+            When an intermediate CA is configured, the response is a PEM blob
+            containing multiple certificates. Each non-root cert must be signed
+            by the next cert in the chain, and the final cert must be 
self-signed.
+        """
+        pem = self.getCaCertificate()
+        self.assertTrue(len(pem) > 0)
+
+        chain = self.parseCertificateChain(pem)
+        self.assertTrue(len(chain) >= 1, "Expected at least one certificate in 
CA chain")
+
+        # Each non-root cert must be signed by the next cert in the chain
+        for i in range(len(chain) - 1):
+            child = chain[i]
+            parent = chain[i + 1]
+            self.assertEqual(
+                child.issuer, parent.subject,
+                f"Chain break: cert[{i}] issuer does not match cert[{i + 1}] 
subject"
+            )
+            try:
+                self.assertSignatureValid(parent, child)
+            except Exception as e:
+                self.fail(f"Signature verification failed for chain link {i} 
-> {i + 1}: {e}")
+
+        # The last cert in the chain must be self-signed (root CA)
+        root = chain[-1]
+        self.assertEqual(
+            root.issuer, root.subject,
+            "Final cert in CA chain is not self-signed"
+        )
+        try:
+            self.assertSignatureValid(root, root)
+        except Exception as e:
+            self.fail(f"Root CA self-signature verification failed: {e}")
+
+
+    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], 
required_hardware=False)
+    def test_issue_certificate_issuer_matches_ca(self):
+        """
+            Tests that an issued certificate's issuer DN matches the subject DN
+            of the first cert in the returned CA chain, and that the signature
+            verifies against that cert's public key.
+        """
+        cmd = issueCertificate.issueCertificateCmd()
+        cmd.domain = 'apache.org'
+        cmd.ipaddress = '10.1.1.1'
+        cmd.provider = 'root'
+
+        response = self.apiclient.issueCertificate(cmd)
+        self.assertTrue(len(response.certificate) > 0)
+        self.assertTrue(len(response.cacertificates) > 0)
+
+        leaf = x509.load_pem_x509_certificate(response.certificate.encode(), 
default_backend())
+        caChain = self.parseCertificateChain(response.cacertificates)
+        self.assertTrue(len(caChain) >= 1, "Expected at least one CA 
certificate in response")
+
+        # The issuing CA is the first cert in the returned chain (intermediate
+        # if an intermediate CA is configured, otherwise the root).
+        issuingCa = caChain[0]
+        self.assertEqual(
+            leaf.issuer, issuingCa.subject,
+            "Leaf certificate issuer does not match issuing CA subject"
+        )
+        try:
+            self.assertSignatureValid(issuingCa, leaf)
+        except Exception as e:
+            self.fail(f"Leaf certificate signature does not verify against 
issuing CA: {e}")
+
+
+    @attr(tags=['advanced', 'simulator', 'basic', 'sg'], 
required_hardware=False)
+    def test_certificate_validity_period(self):
+        """
+            Tests that an issued certificate has sensible validity bounds:
+            not_valid_before <= now <= not_valid_after, and validity duration
+            is at least 300 days (CloudStack default is 1 year).
+        """
+        cmd = issueCertificate.issueCertificateCmd()
+        cmd.domain = 'apache.org'
+        cmd.provider = 'root'
+
+        response = self.apiclient.issueCertificate(cmd)
+        self.assertTrue(len(response.certificate) > 0)
+
+        cert = x509.load_pem_x509_certificate(response.certificate.encode(), 
default_backend())
+
+        # cryptography >= 42 prefers the *_utc variants; fall back for older 
versions.
+        notBefore = getattr(cert, 'not_valid_before_utc', None) or 
cert.not_valid_before
+        notAfter = getattr(cert, 'not_valid_after_utc', None) or 
cert.not_valid_after
+
+        now = datetime.now(notBefore.tzinfo) if notBefore.tzinfo else 
datetime.utcnow()
+        self.assertTrue(notBefore <= now, f"Certificate not_valid_before 
{notBefore} is in the future")
+        self.assertTrue(now <= notAfter, f"Certificate not_valid_after 
{notAfter} is in the past")
+
+        duration = notAfter - notBefore
+        self.assertTrue(
+            duration >= timedelta(days=300),
+            f"Certificate validity duration {duration} is less than expected 
minimum of 300 days"
+        )
+
+
+    def getUpKVMHosts(self, hostId=None):
+        hosts = list_hosts(
+            self.apiclient,
+            type='Routing',
+            hypervisor='KVM',
+            state='Up',
+            resourcestate='Enabled',
+            id=hostId
+        )
+        return hosts
+
+
+    @attr(tags=['advanced'], required_hardware=True)
+    def test_provision_certificate_kvm(self):
+        """
+            Tests certificate provisioning on a KVM host.
+            Exercises the keystore-cert-import + cloud.jks provisioning flow
+            against a real agent. Skipped when no KVM hosts are available.
+        """
+        if self.hypervisor.lower() != 'kvm':
+            raise self.skipTest("Hypervisor is not KVM, skipping test")
+
+        hosts = self.getUpKVMHosts()
+        if not hosts or len(hosts) < 1:
+            raise self.skipTest("No Up KVM hosts found, skipping test")
+
+        host = hosts[0]
+
+        cmd = provisionCertificate.provisionCertificateCmd()
+        cmd.hostid = host.id
+        cmd.reconnect = True
+        cmd.provider = 'root'
+
+        response = self.apiclient.provisionCertificate(cmd)
+        self.assertTrue(response.success)
+
+        def checkHostIsUp(hostId):
+            hosts = self.getUpKVMHosts(hostId)
+            return (hosts is not None and len(hosts) > 0), hosts
+
+        result, hosts = wait_until(2, 30, checkHostIsUp, host.id)
+        if not result:
+            self.fail("KVM host did not return to Up state after certificate 
provisioning")
+        self.assertEqual(len(hosts), 1)
diff --git a/ui/src/config/section/infra/hosts.js 
b/ui/src/config/section/infra/hosts.js
index 2f27db5780b..48e850a22fb 100644
--- a/ui/src/config/section/infra/hosts.js
+++ b/ui/src/config/section/infra/hosts.js
@@ -103,7 +103,7 @@ export default {
       show: (record) => {
         return record.hypervisor === 'KVM' || record.hypervisor === 
store.getters.customHypervisorName
       },
-      args: ['hostid'],
+      args: ['hostid', 'forced'],
       mapping: {
         hostid: {
           value: (record) => { return record.id }
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/CertUtils.java 
b/utils/src/main/java/org/apache/cloudstack/utils/security/CertUtils.java
index 6ff3d918f43..84a4a127440 100644
--- a/utils/src/main/java/org/apache/cloudstack/utils/security/CertUtils.java
+++ b/utils/src/main/java/org/apache/cloudstack/utils/security/CertUtils.java
@@ -98,9 +98,18 @@ public class CertUtils {
         return keyFactory;
     }
 
-    public static X509Certificate pemToX509Certificate(final String pem) 
throws CertificateException, IOException {
-        final PEMParser pemParser = new PEMParser(new StringReader(pem));
-        return new 
JcaX509CertificateConverter().setProvider("BC").getCertificate((X509CertificateHolder)
 pemParser.readObject());
+    public static List<X509Certificate> pemToX509Certificates(final String 
pem) throws CertificateException, IOException {
+        final List<X509Certificate> certs = new ArrayList<>();
+        try (final PEMParser pemParser = new PEMParser(new StringReader(pem))) 
{
+            final JcaX509CertificateConverter certConverter = new 
JcaX509CertificateConverter().setProvider("BC");
+            Object parsedObj;
+            while ((parsedObj = pemParser.readObject()) != null) {
+                if (parsedObj instanceof X509CertificateHolder) {
+                    
certs.add(certConverter.getCertificate((X509CertificateHolder) parsedObj));
+                }
+            }
+        }
+        return certs;
     }
 
     public static String x509CertificateToPem(final X509Certificate cert) 
throws IOException {
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";
diff --git 
a/utils/src/test/java/org/apache/cloudstack/utils/security/CertUtilsTest.java 
b/utils/src/test/java/org/apache/cloudstack/utils/security/CertUtilsTest.java
index 691e7ea0f23..8141c918c8a 100644
--- 
a/utils/src/test/java/org/apache/cloudstack/utils/security/CertUtilsTest.java
+++ 
b/utils/src/test/java/org/apache/cloudstack/utils/security/CertUtilsTest.java
@@ -53,7 +53,7 @@ public class CertUtilsTest {
     public void testCertificateConversionMethods() throws Exception {
         final X509Certificate in = caCertificate;
         final String pem = CertUtils.x509CertificateToPem(in);
-        final X509Certificate out = CertUtils.pemToX509Certificate(pem);
+        final X509Certificate out = 
CertUtils.pemToX509Certificates(pem).get(0);
         Assert.assertTrue(pem.startsWith("-----BEGIN CERTIFICATE-----\n"));
         Assert.assertTrue(pem.endsWith("-----END CERTIFICATE-----\n"));
         Assert.assertEquals(in.getSerialNumber(), out.getSerialNumber());
@@ -87,6 +87,21 @@ public class CertUtilsTest {
         Assert.assertNotEquals(CertUtils.generateRandomBigInt(), 
CertUtils.generateRandomBigInt());
     }
 
+    @Test
+    public void testPemToX509CertificatesWithChain() throws Exception {
+        final KeyPair intermediateKeyPair = 
CertUtils.generateRandomKeyPair(1024);
+        final X509Certificate intermediateCert = 
CertUtils.generateV3Certificate(caCertificate, caKeyPair,
+                intermediateKeyPair.getPublic(), "CN=intermediate", 
"SHA256withRSA", 365, null, null);
+
+        final String chainPem = 
CertUtils.x509CertificateToPem(intermediateCert)
+                + CertUtils.x509CertificateToPem(caCertificate);
+        final List<X509Certificate> parsed = 
CertUtils.pemToX509Certificates(chainPem);
+
+        Assert.assertEquals(2, parsed.size());
+        Assert.assertEquals(intermediateCert.getSerialNumber(), 
parsed.get(0).getSerialNumber());
+        Assert.assertEquals(caCertificate.getSerialNumber(), 
parsed.get(1).getSerialNumber());
+    }
+
     @Test
     public void testGenerateCertificate() throws Exception {
         final KeyPair clientKeyPair = CertUtils.generateRandomKeyPair(1024);

Reply via email to