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);