This commit is contained in:
Vishesh 2026-05-12 13:01:59 +00:00 committed by GitHub
commit 24f0e7e6e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 980 additions and 150 deletions

View File

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

View File

@ -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

View File

@ -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]);
}
}

View File

@ -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> 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 ConfigKey<>("Hidden", String.class,
"ca.plugin.root.public.key",
null,
"The ROOT CA public key.", true);
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 ConfigKey<>("Hidden", String.class,
"ca.plugin.root.ca.certificate",
null,
"The ROOT CA certificate.", true);
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();
@ -322,24 +328,26 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
return loadRootCAKeyPair();
}
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() {
@ -355,14 +363,32 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
return caKeyPair.getPrivate() != null && caKeyPair.getPublic() != null;
}
private boolean loadRootCACertificate() {
boolean loadRootCACertificate() {
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 +415,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 +453,63 @@ public final class RootCAProvider extends AdapterBase implements CAProvider, Con
}
private boolean setupCA() {
if (!loadRootCAKeyPair() && !saveNewRootCAKeypair()) {
logger.error("Failed to save and load root CA keypair");
return false;
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());
}
if (!loadRootCACertificate() && !saveNewRootCACertificate()) {
logger.error("Failed to save and load root CA certificate");
return false;
}
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()) {
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()) {
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 +518,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

View File

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

View File

@ -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"));
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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.");

View File

@ -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
};
}

View File

@ -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());
}

View File

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

View File

@ -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

View File

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

View File

@ -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 }

View File

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

View File

@ -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 {

View File

@ -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";

View File

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