From 7ce54bf7a85d6df72f84c00fadf9b0fd42ab0d99 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 28 Aug 2017 12:15:11 +0200 Subject: [PATCH] CLOUDSTACK-9993: Securing Agents Communications (#2239) This introduces a new certificate authority framework that allows pluggable CA provider implementations to handle certificate operations around issuance, revocation and propagation. The framework injects itself to `NioServer` to handle agent connections securely. The framework adds assumptions in `NioClient` that a keystore if available with known name `cloud.jks` will be used for SSL negotiations and handshake. This includes a default 'root' CA provider plugin which creates its own self-signed root certificate authority on first run and uses it for issuance and provisioning of certificate to CloudStack agents such as the KVM, CPVM and SSVM agents and also for the management server for peer clustering. Additional changes and notes: - Comma separate list of management server IPs can be set to the 'host' global setting. Newly provisioned agents (KVM/CPVM/SSVM etc) will get radomized comma separated list to which they will attempt connection or reconnection in provided order. This removes need of a TCP LB on port 8250 (default) of the management server(s). - All fresh deployment will enforce two-way SSL authentication where connecting agents will be required to present certificates issued by the 'root' CA plugin. - Existing environment on upgrade will continue to use one-way SSL authentication and connecting agents will not be required to present certificates. - A script `keystore-setup` is responsible for initial keystore setup and CSR generation on the agent/hosts. - A script `keystore-cert-import` is responsible for import provided certificate payload to the java keystore file. - Agent security (keystore, certificates etc) are setup initially using SSH, and later provisioning is handled via an existing agent connection using command-answers. The supported clients and agents are limited to CPVM, SSVM, and KVM agents, and clustered management server (peering). - Certificate revocation does not revoke an existing agent-mgmt server connection, however rejects a revoked certificate used during SSL handshake. - Older `cloudstackmanagement.keystore` is deprecated and will no longer be used by mgmt server(s) for SSL negotiations and handshake. New keystores will be named `cloud.jks`, any additional SSL certificates should not be imported in it for use with tomcat etc. The `cloud.jks` keystore is stricly used for agent-server communications. - Management server keystore are validated and renewed on start up only, the validity of them are same as the CA certificates. New APIs: - listCaProviders: lists all available CA provider plugins - listCaCertificate: lists the CA certificate(s) - issueCertificate: issues X509 client certificate with/without a CSR - provisionCertificate: provisions certificate to a host - revokeCertificate: revokes a client certificate using its serial Global settings for the CA framework: - ca.framework.provider.plugin: The configured CA provider plugin - ca.framework.cert.keysize: The key size for certificate generation - ca.framework.cert.signature.algorithm: The certificate signature algorithm - ca.framework.cert.validity.period: Certificate validity in days - ca.framework.cert.automatic.renewal: Certificate auto-renewal setting - ca.framework.background.task.delay: CA background task delay/interval - ca.framework.cert.expiry.alert.period: Days to check and alert expiring certificates Global settings for the default 'root' CA provider: - ca.plugin.root.private.key: (hidden/encrypted) CA private key - ca.plugin.root.public.key: (hidden/encrypted) CA public key - ca.plugin.root.ca.certificate: (hidden/encrypted) CA certificate - ca.plugin.root.issuer.dn: The CA issue distinguished name - ca.plugin.root.auth.strictness: Are clients required to present certificates - ca.plugin.root.allow.expired.cert: Are clients with expired certificates allowed UI changes: - Button to download/save the CA certificates. Misc changes: - Upgrades bountycastle version and uses newer classes - Refactors SAMLUtil to use new CertUtils Signed-off-by: Rohit Yadav --- .travis.yml | 1 + agent/src/com/cloud/agent/Agent.java | 127 ++++- agent/src/com/cloud/agent/AgentShell.java | 13 +- .../agent/dao/impl/PropertiesStorage.java | 29 +- .../consoleproxy/ConsoleProxyResource.java | 12 +- .../test/com/cloud/agent/AgentShellTest.java | 15 + api/pom.xml | 5 + api/src/com/cloud/event/EventTypes.java | 22 +- .../apache/cloudstack/alert/AlertService.java | 1 + .../apache/cloudstack/api/ApiConstants.java | 5 + .../command/admin/ca/IssueCertificateCmd.java | 162 ++++++ .../command/admin/ca/ListCAProvidersCmd.java | 102 ++++ .../admin/ca/ListCaCertificateCmd.java | 90 ++++ .../admin/ca/ProvisionCertificateCmd.java | 125 +++++ .../admin/ca/RevokeCertificateCmd.java | 116 +++++ .../api/response/CAProviderResponse.java | 52 ++ .../api/response/CertificateResponse.java | 58 +++ .../org/apache/cloudstack/ca/CAManager.java | 163 ++++++ .../cloudstack/poll/BackgroundPollTask.java | 6 + client/pom.xml | 11 + .../tomcatconf/cloudmanagementserver.keystore | Bin 1316 -> 0 bytes client/tomcatconf/server-ssl.xml.in | 2 +- client/tomcatconf/server7-ssl.xml.in | 2 +- client/tomcatconf/tomcat6-ssl.conf.in | 2 +- .../META-INF/cloudstack/ca/module.properties | 21 + ...-core-lifecycle-ca-context-inheritable.xml | 32 ++ .../spring-core-registry-core-context.xml | 4 + .../api/routing/NetworkElementCommand.java | 13 + .../VirtualRoutingResource.java | 44 ++ .../cloudstack/ca/SetupCertificateAnswer.java | 29 ++ .../ca/SetupCertificateCommand.java | 99 ++++ .../cloudstack/ca/SetupKeyStoreCommand.java | 75 +++ .../cloudstack/ca/SetupKeystoreAnswer.java | 37 ++ debian/cloudstack-management.postinst | 3 - developer/developer-prefill.sql | 5 + .../service/NetworkOrchestrationService.java | 2 + .../cloud/agent/manager/AgentManagerImpl.java | 6 +- .../manager/ClusteredAgentManagerImpl.java | 21 +- .../cloud/vm/VirtualMachineManagerImpl.java | 27 +- .../orchestration/NetworkOrchestrator.java | 35 ++ ...spring-engine-schema-core-daos-context.xml | 1 + .../src/com/cloud/certificate/CrlVO.java | 85 ++++ .../src/com/cloud/certificate/dao/CrlDao.java | 28 ++ .../com/cloud/certificate/dao/CrlDaoImpl.java | 57 +++ .../src/com/cloud/host/dao/HostDao.java | 2 + .../src/com/cloud/host/dao/HostDaoImpl.java | 13 + framework/ca/pom.xml | 29 ++ .../cloudstack/framework/ca/CAProvider.java | 93 ++++ .../cloudstack/framework/ca/CAService.java | 36 ++ .../cloudstack/framework/ca/Certificate.java | 46 ++ framework/pom.xml | 1 + packaging/centos63/cloud.spec | 6 - packaging/fedora20/cloud.spec | 6 - packaging/fedora21/cloud.spec | 6 - .../systemd/cloudstack-management.default | 4 +- .../cloudstack-management.default.ubuntu | 4 +- plugins/ca/root-ca/pom.xml | 46 ++ .../cloudstack/root-ca/module.properties | 18 + .../root-ca/spring-root-ca-context.xml | 29 ++ .../ca/provider/RootCACustomTrustManager.java | 146 ++++++ .../ca/provider/RootCAProvider.java | 465 ++++++++++++++++++ .../RootCACustomTrustManagerTest.java | 111 +++++ .../ca/provider/RootCAProviderTest.java | 155 ++++++ .../cloud/agent/manager/MockAgentManager.java | 7 + .../agent/manager/MockAgentManagerImpl.java | 24 +- .../agent/manager/SimulatorManagerImpl.java | 6 + plugins/pom.xml | 1 + .../cloudstack/saml/SAML2AuthManagerImpl.java | 99 ++-- .../org/apache/cloudstack/saml/SAMLUtils.java | 158 ++---- .../GetServiceProviderMetaDataCmdTest.java | 30 +- .../org/apache/cloudstack/SAMLUtilsTest.java | 20 +- .../SAML2LoginAPIAuthenticatorCmdTest.java | 35 +- .../SAML2LogoutAPIAuthenticatorCmdTest.java | 19 +- pom.xml | 7 +- scripts/common/keys/ssl-keys.py | 58 --- scripts/installer/windows/acs.wxs | 3 - scripts/installer/windows/client.wxs | 3 - scripts/network/domr/router_proxy.sh | 10 +- scripts/util/keystore-cert-import | 100 ++++ scripts/util/keystore-setup | 51 ++ server/pom.xml | 5 + .../spring-server-core-managers-context.xml | 8 + .../src/com/cloud/alert/AlertManagerImpl.java | 3 +- .../consoleproxy/ConsoleProxyManagerImpl.java | 3 +- .../discoverer/LibvirtServerDiscoverer.java | 116 ++++- .../cloud/resource/ResourceManagerImpl.java | 3 +- .../cloud/server/ConfigurationServerImpl.java | 126 +---- .../apache/cloudstack/ca/CAManagerImpl.java | 428 ++++++++++++++++ .../OutOfBandManagementServiceImpl.java | 6 + .../poll/BackgroundPollManagerImpl.java | 6 +- .../server/ConfigurationServerImplTest.java | 61 +-- .../com/cloud/vpc/MockNetworkManagerImpl.java | 5 + .../cloudstack/ca/CABackgroundTaskTest.java | 152 ++++++ .../cloudstack/ca/CAManagerImplTest.java | 119 +++++ .../poll/BackgroundPollManagerImplTest.java | 6 + .../SecondaryStorageManagerImpl.java | 3 +- .../resource/NfsSecondaryStorageResource.java | 136 ++--- setup/db/db/schema-41000to41100.sql | 15 + setup/db/server-setup.sql | 3 + setup/db/server-setup.xml | 7 + .../config/opt/cloud/bin/patchsystemvm.sh | 4 - systemvm/pom.xml | 5 + systemvm/systemvm-descriptor.xml | 9 + .../smoke/test_certauthority_root.py | 229 +++++++++ tools/apidoc/gen_toc.py | 3 +- tools/travis/before_install.sh | 7 +- ui/css/cloudstack3.css | 27 + ui/images/sprites.png | Bin 198421 -> 207062 bytes ui/index.html | 1 + ui/scripts/ui-custom/ca.js | 53 ++ utils/pom.xml | 9 + .../java/com/cloud/utils/StringUtils.java | 8 + .../exception/TaskExecutionException.java | 2 +- .../main/java/com/cloud/utils/nio/Link.java | 175 ++++--- .../java/com/cloud/utils/nio/NioClient.java | 2 +- .../com/cloud/utils/nio/NioConnection.java | 27 +- .../java/com/cloud/utils/nio/NioServer.java | 4 +- .../java/com/cloud/utils/script/Script.java | 3 +- .../com/cloud/utils/ssh/SSHCmdHelper.java | 93 +++- .../cloudstack/utils/security/CertUtils.java | 240 +++++++++ .../utils/security/KeyStoreUtils.java | 70 +++ .../java/com/cloud/utils/StringUtilsTest.java | 13 + .../com/cloud/utils/testcase/NioTest.java | 2 +- .../utils/security/CertUtilsTest.java | 118 +++++ 124 files changed, 5151 insertions(+), 756 deletions(-) create mode 100644 api/src/org/apache/cloudstack/api/command/admin/ca/IssueCertificateCmd.java create mode 100644 api/src/org/apache/cloudstack/api/command/admin/ca/ListCAProvidersCmd.java create mode 100644 api/src/org/apache/cloudstack/api/command/admin/ca/ListCaCertificateCmd.java create mode 100644 api/src/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java create mode 100644 api/src/org/apache/cloudstack/api/command/admin/ca/RevokeCertificateCmd.java create mode 100644 api/src/org/apache/cloudstack/api/response/CAProviderResponse.java create mode 100644 api/src/org/apache/cloudstack/api/response/CertificateResponse.java create mode 100644 api/src/org/apache/cloudstack/ca/CAManager.java delete mode 100644 client/tomcatconf/cloudmanagementserver.keystore create mode 100644 core/resources/META-INF/cloudstack/ca/module.properties create mode 100644 core/resources/META-INF/cloudstack/ca/spring-core-lifecycle-ca-context-inheritable.xml create mode 100644 core/src/org/apache/cloudstack/ca/SetupCertificateAnswer.java create mode 100644 core/src/org/apache/cloudstack/ca/SetupCertificateCommand.java create mode 100644 core/src/org/apache/cloudstack/ca/SetupKeyStoreCommand.java create mode 100644 core/src/org/apache/cloudstack/ca/SetupKeystoreAnswer.java create mode 100644 engine/schema/src/com/cloud/certificate/CrlVO.java create mode 100644 engine/schema/src/com/cloud/certificate/dao/CrlDao.java create mode 100644 engine/schema/src/com/cloud/certificate/dao/CrlDaoImpl.java create mode 100644 framework/ca/pom.xml create mode 100644 framework/ca/src/org/apache/cloudstack/framework/ca/CAProvider.java create mode 100644 framework/ca/src/org/apache/cloudstack/framework/ca/CAService.java create mode 100644 framework/ca/src/org/apache/cloudstack/framework/ca/Certificate.java create mode 100644 plugins/ca/root-ca/pom.xml create mode 100644 plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/module.properties create mode 100644 plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/spring-root-ca-context.xml create mode 100644 plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java create mode 100644 plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCAProvider.java create mode 100644 plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java create mode 100644 plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCAProviderTest.java delete mode 100644 scripts/common/keys/ssl-keys.py create mode 100755 scripts/util/keystore-cert-import create mode 100755 scripts/util/keystore-setup create mode 100644 server/src/org/apache/cloudstack/ca/CAManagerImpl.java create mode 100644 server/test/org/apache/cloudstack/ca/CABackgroundTaskTest.java create mode 100644 server/test/org/apache/cloudstack/ca/CAManagerImplTest.java create mode 100644 test/integration/smoke/test_certauthority_root.py create mode 100644 ui/scripts/ui-custom/ca.js create mode 100644 utils/src/main/java/org/apache/cloudstack/utils/security/CertUtils.java create mode 100644 utils/src/main/java/org/apache/cloudstack/utils/security/KeyStoreUtils.java create mode 100644 utils/src/test/java/org/apache/cloudstack/utils/security/CertUtilsTest.java diff --git a/.travis.yml b/.travis.yml index 4301d7569cd7..b25becb6e05f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,6 +34,7 @@ env: matrix: - TESTS="smoke/test_affinity_groups smoke/test_affinity_groups_projects + smoke/test_certauthority_root smoke/test_deploy_vgpu_enabled_vm smoke/test_deploy_vm_iso smoke/test_deploy_vm_root_resize diff --git a/agent/src/com/cloud/agent/Agent.java b/agent/src/com/cloud/agent/Agent.java index 7fab5f4d3013..7e802205f5c1 100644 --- a/agent/src/com/cloud/agent/Agent.java +++ b/agent/src/com/cloud/agent/Agent.java @@ -16,12 +16,14 @@ // under the License. package com.cloud.agent; +import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.channels.ClosedChannelException; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -35,7 +37,13 @@ import javax.naming.ConfigurationException; +import org.apache.cloudstack.ca.SetupCertificateAnswer; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.ca.SetupKeyStoreCommand; +import org.apache.cloudstack.ca.SetupKeystoreAnswer; import org.apache.cloudstack.managed.context.ManagedContextTimerTask; +import org.apache.cloudstack.utils.security.KeyStoreUtils; +import org.apache.commons.io.FileUtils; import org.apache.log4j.Logger; import org.slf4j.MDC; @@ -68,6 +76,7 @@ import com.cloud.utils.nio.Task; import com.cloud.utils.script.OutputInterpreter; import com.cloud.utils.script.Script; +import com.google.common.base.Strings; /** * @config @@ -126,6 +135,9 @@ public int value() { private final ThreadPoolExecutor _ugentTaskPool; ExecutorService _executor; + private String _keystoreSetupPath; + private String _keystoreCertImportPath; + // for simulator use only public Agent(final IAgentShell shell) { _shell = shell; @@ -166,7 +178,8 @@ public Agent(final IAgentShell shell, final int localAgentId, final ServerResour throw new ConfigurationException("Unable to configure " + _resource.getName()); } - _connection = new NioClient("Agent", _shell.getHost(), _shell.getPort(), _shell.getWorkers(), this); + final String host = _shell.getHost(); + _connection = new NioClient("Agent", host, _shell.getPort(), _shell.getWorkers(), this); // ((NioClient)_connection).setBindAddress(_shell.getPrivateIp()); @@ -182,7 +195,7 @@ public Agent(final IAgentShell shell, final int localAgentId, final ServerResour "agentRequest-Handler")); s_logger.info("Agent [id = " + (_id != null ? _id : "new") + " : type = " + getResourceName() + " : zone = " + _shell.getZone() + " : pod = " + _shell.getPod() + - " : workers = " + _shell.getWorkers() + " : host = " + _shell.getHost() + " : port = " + _shell.getPort()); + " : workers = " + _shell.getWorkers() + " : host = " + host + " : port = " + _shell.getPort()); } public String getVersion() { @@ -224,6 +237,16 @@ public void start() { throw new CloudRuntimeException("Unable to start the resource: " + _resource.getName()); } + _keystoreSetupPath = Script.findScript("scripts/util/", KeyStoreUtils.keyStoreSetupScript); + if (_keystoreSetupPath == null) { + throw new CloudRuntimeException(String.format("Unable to find the '%s' script", KeyStoreUtils.keyStoreSetupScript)); + } + + _keystoreCertImportPath = Script.findScript("scripts/util/", KeyStoreUtils.keyStoreImportScript); + if (_keystoreCertImportPath == null) { + throw new CloudRuntimeException(String.format("Unable to find the '%s' script", KeyStoreUtils.keyStoreImportScript)); + } + try { _connection.start(); } catch (final NioConnectionException e) { @@ -231,8 +254,10 @@ public void start() { s_logger.info("Attempted to connect to the server, but received an unexpected exception, trying again..."); } while (!_connection.isStartup()) { + final String host = _shell.getHost(); _shell.getBackoffAlgorithm().waitBeforeRetry(); - _connection = new NioClient("Agent", _shell.getHost(), _shell.getPort(), _shell.getWorkers(), this); + _connection = new NioClient("Agent", host, _shell.getPort(), _shell.getWorkers(), this); + s_logger.info("Connecting to host:" + host); try { _connection.start(); } catch (final NioConnectionException e) { @@ -408,14 +433,21 @@ protected void reconnect(final Link link) { _shell.getBackoffAlgorithm().waitBeforeRetry(); } - _connection = new NioClient("Agent", _shell.getHost(), _shell.getPort(), _shell.getWorkers(), this); + final String host = _shell.getHost(); do { - s_logger.info("Reconnecting..."); + _connection = new NioClient("Agent", host, _shell.getPort(), _shell.getWorkers(), this); + s_logger.info("Reconnecting to host:" + host); try { _connection.start(); } catch (final NioConnectionException e) { s_logger.warn("NIO Connection Exception " + e); s_logger.info("Attempted to connect to the server, but received an unexpected exception, trying again..."); + _connection.stop(); + try { + _connection.cleanUp(); + } catch (final IOException ex) { + s_logger.warn("Fail to clean up old connection. " + ex); + } } _shell.getBackoffAlgorithm().waitBeforeRetry(); } while (!_connection.isStartup()); @@ -515,7 +547,10 @@ protected void processRequest(final Request request, final Link link) { s_logger.warn("No handler found to process cmd: " + cmd.toString()); answer = new AgentControlAnswer(cmd); } - + } else if (cmd instanceof SetupKeyStoreCommand && ((SetupKeyStoreCommand) cmd).isHandleByAgent()) { + answer = setupAgentKeystore((SetupKeyStoreCommand) cmd); + } else if (cmd instanceof SetupCertificateCommand && ((SetupCertificateCommand) cmd).isHandleByAgent()) { + answer = setupAgentCertificate((SetupCertificateCommand) cmd); } else { if (cmd instanceof ReadyCommand) { processReadyCommand(cmd); @@ -565,6 +600,86 @@ protected void processRequest(final Request request, final Link link) { } } + public Answer setupAgentKeystore(final SetupKeyStoreCommand cmd) { + final String keyStorePassword = cmd.getKeystorePassword(); + final long validityDays = cmd.getValidityDays(); + + s_logger.debug("Setting up agent keystore file and generating CSR"); + + final File agentFile = PropertiesUtil.findConfigFile("agent.properties"); + if (agentFile == null) { + return new Answer(cmd, false, "Failed to find agent.properties file"); + } + final String keyStoreFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultKeystoreFile; + final String csrFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultCsrFile; + + String storedPassword = _shell.getPersistentProperty(null, KeyStoreUtils.passphrasePropertyName); + if (Strings.isNullOrEmpty(storedPassword)) { + storedPassword = keyStorePassword; + _shell.setPersistentProperty(null, KeyStoreUtils.passphrasePropertyName, storedPassword); + } + + Script script = new Script(_keystoreSetupPath, 60000, s_logger); + script.add(agentFile.getAbsolutePath()); + script.add(keyStoreFile); + script.add(storedPassword); + script.add(String.valueOf(validityDays)); + script.add(csrFile); + String result = script.execute(); + if (result != null) { + throw new CloudRuntimeException("Unable to setup keystore file"); + } + + final String csrString; + try { + csrString = FileUtils.readFileToString(new File(csrFile), Charset.defaultCharset()); + } catch (IOException e) { + throw new CloudRuntimeException("Unable to read generated CSR file", e); + } + return new SetupKeystoreAnswer(csrString); + } + + private Answer setupAgentCertificate(final SetupCertificateCommand cmd) { + final String certificate = cmd.getCertificate(); + final String privateKey = cmd.getPrivateKey(); + final String caCertificates = cmd.getCaCertificates(); + + s_logger.debug("Importing received certificate to agent's keystore"); + + final File agentFile = PropertiesUtil.findConfigFile("agent.properties"); + if (agentFile == null) { + return new Answer(cmd, false, "Failed to find agent.properties file"); + } + final String keyStoreFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultKeystoreFile; + final String certFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultCertFile; + final String privateKeyFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultPrivateKeyFile; + final String caCertFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultCaCertFile; + + try { + FileUtils.writeStringToFile(new File(certFile), certificate, Charset.defaultCharset()); + FileUtils.writeStringToFile(new File(caCertFile), caCertificates, Charset.defaultCharset()); + s_logger.debug("Saved received client certificate to: " + certFile); + } catch (IOException e) { + throw new CloudRuntimeException("Unable to save received agent client and ca certificates", e); + } + + Script script = new Script(_keystoreCertImportPath, 60000, s_logger); + script.add(agentFile.getAbsolutePath()); + script.add(keyStoreFile); + script.add(KeyStoreUtils.agentMode); + script.add(certFile); + script.add(""); + script.add(caCertFile); + script.add(""); + script.add(privateKeyFile); + script.add(privateKey); + String result = script.execute(); + if (result != null) { + throw new CloudRuntimeException("Unable to import certificate into keystore file"); + } + return new SetupCertificateAnswer(true); + } + public void processResponse(final Response response, final Link link) { final Answer answer = response.getAnswer(); if (s_logger.isDebugEnabled()) { diff --git a/agent/src/com/cloud/agent/AgentShell.java b/agent/src/com/cloud/agent/AgentShell.java index 5e0da68c6d66..5950bc78e619 100644 --- a/agent/src/com/cloud/agent/AgentShell.java +++ b/agent/src/com/cloud/agent/AgentShell.java @@ -67,6 +67,7 @@ public class AgentShell implements IAgentShell, Daemon { private int _proxyPort; private int _workers; private String _guid; + private int _hostCounter = 0; private int _nextAgentId = 1; private volatile boolean _exit = false; private int _pingRetries; @@ -107,7 +108,17 @@ public String getPod() { @Override public String getHost() { - return _host; + final String[] hosts = _host.split(","); + if (_hostCounter >= hosts.length) { + _hostCounter = 0; + } + final String host = hosts[_hostCounter % hosts.length]; + _hostCounter++; + return host; + } + + public void setHost(final String host) { + _host = host; } @Override diff --git a/agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java b/agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java index df1b1ea7b271..e9eac645cb76 100644 --- a/agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java +++ b/agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java @@ -51,6 +51,9 @@ public synchronized String get(String key) { @Override public synchronized void persist(String key, String value) { + if (!loadFromFile(_file)) { + s_logger.error("Failed to load changes and then write to them"); + } _properties.setProperty(key, value); FileOutputStream output = null; try { @@ -65,6 +68,20 @@ public synchronized void persist(String key, String value) { } } + private synchronized boolean loadFromFile(final File file) { + try { + PropertiesUtil.loadFromFile(_properties, file); + _file = file; + } catch (FileNotFoundException e) { + s_logger.error("How did we get here? ", e); + return false; + } catch (IOException e) { + s_logger.error("IOException: ", e); + return false; + } + return true; + } + @Override public synchronized boolean configure(String name, Map params) { _name = name; @@ -86,17 +103,7 @@ public synchronized boolean configure(String name, Map params) { return false; } } - try { - PropertiesUtil.loadFromFile(_properties, file); - _file = file; - } catch (FileNotFoundException e) { - s_logger.error("How did we get here? ", e); - return false; - } catch (IOException e) { - s_logger.error("IOException: ", e); - return false; - } - return true; + return loadFromFile(file); } @Override diff --git a/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java b/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java index 08f098239865..1fed3be753ca 100644 --- a/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java +++ b/agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java @@ -32,11 +32,8 @@ import javax.naming.ConfigurationException; -import org.apache.log4j.Logger; - -import com.google.gson.Gson; - import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.log4j.Logger; import com.cloud.agent.Agent.ExitStatus; import com.cloud.agent.api.AgentControlAnswer; @@ -64,6 +61,7 @@ import com.cloud.utils.NumbersUtil; import com.cloud.utils.net.NetUtils; import com.cloud.utils.script.Script; +import com.google.gson.Gson; /** * @@ -240,9 +238,11 @@ public boolean configure(String name, Map params) throws Configu _proxyVmId = NumbersUtil.parseLong(value, 0); if (_localgw != null) { - String mgmtHost = (String)params.get("host"); + String mgmtHosts = (String)params.get("host"); if (_eth1ip != null) { - addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost); + for (final String mgmtHost : mgmtHosts.split(",")) { + addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost); + } String internalDns1 = (String) params.get("internaldns1"); if (internalDns1 == null) { s_logger.warn("No DNS entry found during configuration of NfsSecondaryStorage"); diff --git a/agent/test/com/cloud/agent/AgentShellTest.java b/agent/test/com/cloud/agent/AgentShellTest.java index 5baa7bf800e9..8ceba4531d1c 100644 --- a/agent/test/com/cloud/agent/AgentShellTest.java +++ b/agent/test/com/cloud/agent/AgentShellTest.java @@ -16,6 +16,8 @@ // under the License. package com.cloud.agent; +import java.util.Arrays; +import java.util.List; import java.util.UUID; import javax.naming.ConfigurationException; @@ -23,6 +25,8 @@ import org.junit.Assert; import org.junit.Test; +import com.cloud.utils.StringUtils; + public class AgentShellTest { @Test public void parseCommand() throws ConfigurationException { @@ -44,4 +48,15 @@ public void loadProperties() throws ConfigurationException { Assert.assertNotNull(shell.getProperties()); Assert.assertFalse(shell.getProperties().entrySet().isEmpty()); } + + @Test + public void testGetHost() { + AgentShell shell = new AgentShell(); + List hosts = Arrays.asList("10.1.1.1", "20.2.2.2", "30.3.3.3", "2001:db8::1"); + shell.setHost(StringUtils.listToCsvTags(hosts)); + for (String host : hosts) { + Assert.assertEquals(host, shell.getHost()); + } + Assert.assertEquals(shell.getHost(), hosts.get(0)); + } } diff --git a/api/pom.xml b/api/pom.xml index 93859f1b40d7..6352e117bfef 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -51,6 +51,11 @@ cloud-framework-config ${project.version} + + org.apache.cloudstack + cloud-framework-ca + ${project.version} + diff --git a/api/src/com/cloud/event/EventTypes.java b/api/src/com/cloud/event/EventTypes.java index 32a3138172f0..66bfbfe6d288 100644 --- a/api/src/com/cloud/event/EventTypes.java +++ b/api/src/com/cloud/event/EventTypes.java @@ -16,6 +16,14 @@ // under the License. package com.cloud.event; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.acl.Role; +import org.apache.cloudstack.acl.RolePermission; +import org.apache.cloudstack.config.Configuration; +import org.apache.cloudstack.usage.Usage; + import com.cloud.dc.DataCenter; import com.cloud.dc.Pod; import com.cloud.dc.StorageNetworkIpRange; @@ -54,10 +62,10 @@ import com.cloud.offering.ServiceOffering; import com.cloud.projects.Project; import com.cloud.server.ResourceTag; -import com.cloud.storage.StoragePool; import com.cloud.storage.GuestOS; import com.cloud.storage.GuestOSHypervisor; import com.cloud.storage.Snapshot; +import com.cloud.storage.StoragePool; import com.cloud.storage.Volume; import com.cloud.storage.snapshot.SnapshotPolicy; import com.cloud.template.VirtualMachineTemplate; @@ -66,13 +74,6 @@ import com.cloud.vm.Nic; import com.cloud.vm.NicSecondaryIp; import com.cloud.vm.VirtualMachine; -import org.apache.cloudstack.acl.Role; -import org.apache.cloudstack.acl.RolePermission; -import org.apache.cloudstack.config.Configuration; -import org.apache.cloudstack.usage.Usage; - -import java.util.HashMap; -import java.util.Map; public class EventTypes { @@ -176,6 +177,11 @@ public class EventTypes { public static final String EVENT_ROLE_PERMISSION_UPDATE = "ROLE.PERMISSION.UPDATE"; public static final String EVENT_ROLE_PERMISSION_DELETE = "ROLE.PERMISSION.DELETE"; + // CA events + public static final String EVENT_CA_CERTIFICATE_ISSUE = "CA.CERTIFICATE.ISSUE"; + public static final String EVENT_CA_CERTIFICATE_REVOKE = "CA.CERTIFICATE.REVOKE"; + public static final String EVENT_CA_CERTIFICATE_PROVISION = "CA.CERTIFICATE.PROVISION"; + // Account events public static final String EVENT_ACCOUNT_ENABLE = "ACCOUNT.ENABLE"; public static final String EVENT_ACCOUNT_DISABLE = "ACCOUNT.DISABLE"; diff --git a/api/src/org/apache/cloudstack/alert/AlertService.java b/api/src/org/apache/cloudstack/alert/AlertService.java index ad711ecea4e8..841296996eaf 100644 --- a/api/src/org/apache/cloudstack/alert/AlertService.java +++ b/api/src/org/apache/cloudstack/alert/AlertService.java @@ -67,6 +67,7 @@ private AlertType(short type, String name, boolean isDefault) { public static final AlertType ALERT_TYPE_SYNC = new AlertType((short)27, "ALERT.TYPE.SYNC", true); public static final AlertType ALERT_TYPE_UPLOAD_FAILED = new AlertType((short)28, "ALERT.UPLOAD.FAILED", true); public static final AlertType ALERT_TYPE_OOBM_AUTH_ERROR = new AlertType((short)29, "ALERT.OOBM.AUTHERROR", true); + public static final AlertType ALERT_TYPE_CA_CERT = new AlertType((short)31, "ALERT.CA.CERT", true); public short getType() { return type; diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index b77b83a20bf7..da39ff84fae4 100644 --- a/api/src/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/org/apache/cloudstack/api/ApiConstants.java @@ -37,10 +37,12 @@ public class ApiConstants { public static final String BYTES_WRITE_RATE = "byteswriterate"; public static final String CATEGORY = "category"; public static final String CAN_REVERT = "canrevert"; + public static final String CA_CERTIFICATES = "cacertificates"; public static final String CERTIFICATE = "certificate"; public static final String CERTIFICATE_CHAIN = "certchain"; public static final String CERTIFICATE_FINGERPRINT = "fingerprint"; public static final String CERTIFICATE_ID = "certid"; + public static final String CSR = "csr"; public static final String PRIVATE_KEY = "privatekey"; public static final String DOMAIN_SUFFIX = "domainsuffix"; public static final String DNS_SEARCH_ORDER = "dnssearchorder"; @@ -54,6 +56,7 @@ public class ApiConstants { public static final String CLUSTER_ID = "clusterid"; public static final String CLUSTER_NAME = "clustername"; public static final String CLUSTER_TYPE = "clustertype"; + public static final String CN = "cn"; public static final String COMMAND = "command"; public static final String CMD_EVENT_TYPE = "cmdeventtype"; public static final String COMPONENT = "component"; @@ -216,6 +219,7 @@ public class ApiConstants { public static final String PUBLIC_END_PORT = "publicendport"; public static final String PUBLIC_ZONE = "publiczone"; public static final String RECEIVED_BYTES = "receivedbytes"; + public static final String RECONNECT = "reconnect"; public static final String REQUIRES_HVM = "requireshvm"; public static final String RESOURCE_TYPE = "resourcetype"; public static final String RESPONSE = "response"; @@ -234,6 +238,7 @@ public class ApiConstants { public static final String SECURITY_GROUP_ID = "securitygroupid"; public static final String SENT = "sent"; public static final String SENT_BYTES = "sentbytes"; + public static final String SERIAL = "serial"; public static final String SERVICE_OFFERING_ID = "serviceofferingid"; public static final String SESSIONKEY = "sessionkey"; public static final String SHOW_CAPACITIES = "showcapacities"; diff --git a/api/src/org/apache/cloudstack/api/command/admin/ca/IssueCertificateCmd.java b/api/src/org/apache/cloudstack/api/command/admin/ca/IssueCertificateCmd.java new file mode 100644 index 000000000000..8926829205f6 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/ca/IssueCertificateCmd.java @@ -0,0 +1,162 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.ca; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.CertificateResponse; +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.utils.security.CertUtils; +import org.apache.log4j.Logger; + +import com.cloud.event.EventTypes; +import com.google.common.base.Strings; + +@APICommand(name = IssueCertificateCmd.APINAME, + description = "Issues a client certificate using configured or provided CA plugin", + responseObject = CertificateResponse.class, + requestHasSensitiveInfo = true, + responseHasSensitiveInfo = true, + since = "4.11.0", + authorized = {RoleType.Admin}) +public class IssueCertificateCmd extends BaseAsyncCmd { + private static final Logger LOG = Logger.getLogger(IssueCertificateCmd.class); + + public static final String APINAME = "issueCertificate"; + + @Inject + private CAManager caManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.CSR, type = BaseCmd.CommandType.STRING, description = "The certificate signing request (in pem format), if CSR is not provided then configured/provided options are considered", length = 65535) + private String csr; + + @Parameter(name = ApiConstants.DOMAIN, type = BaseCmd.CommandType.STRING, description = "Comma separated list of domains, the certificate should be issued for. When csr is not provided, the first domain is used as a subject/CN") + private String domains; + + @Parameter(name = ApiConstants.IP_ADDRESS, type = BaseCmd.CommandType.STRING, description = "Comma separated list of IP addresses, the certificate should be issued for") + private String addresses; + + @Parameter(name = ApiConstants.DURATION, type = CommandType.INTEGER, description = "Certificate validity duration in number of days, when not provided the default configured value will be used") + private Integer validityDuration; + + @Parameter(name = ApiConstants.PROVIDER, type = BaseCmd.CommandType.STRING, description = "Name of the CA service provider, otherwise the default configured provider plugin will be used") + private String provider; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getCsr() { + return csr; + } + + private List processList(final String string) { + final List list = new ArrayList<>(); + if (!Strings.isNullOrEmpty(string)) { + for (final String address: string.split(",")) { + list.add(address.trim()); + } + } + return list; + } + + public List getAddresses() { + return processList(addresses); + } + + public List getDomains() { + return processList(domains); + } + + public Integer getValidityDuration() { + return validityDuration; + } + + public String getProvider() { + return provider; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + if (Strings.isNullOrEmpty(getCsr()) && getDomains().isEmpty()) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Please provide the domains or the CSR, none of them are provided"); + } + final Certificate certificate = caManager.issueCertificate(getCsr(), getDomains(), getAddresses(), getValidityDuration(), getProvider()); + if (certificate == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to issue client certificate with given provider"); + } + + final CertificateResponse certificateResponse = new CertificateResponse(); + try { + certificateResponse.setCertificate(CertUtils.x509CertificateToPem(certificate.getClientCertificate())); + if (certificate.getPrivateKey() != null) { + certificateResponse.setPrivateKey(CertUtils.privateKeyToPem(certificate.getPrivateKey())); + } + if (certificate.getCaCertificates() != null) { + certificateResponse.setCaCertificate(CertUtils.x509CertificatesToPem(certificate.getCaCertificates())); + } + } catch (final IOException e) { + LOG.error("Failed to generate and convert client certificate(s) to PEM due to error: ", e); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to process and return client certificate"); + } + certificateResponse.setResponseName(getCommandName()); + setResponseObject(certificateResponse); + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_CA_CERTIFICATE_ISSUE; + } + + @Override + public String getEventDescription() { + return "issuing certificate for domain(s)=" + domains + ", ip(s)=" + addresses; + } +} diff --git a/api/src/org/apache/cloudstack/api/command/admin/ca/ListCAProvidersCmd.java b/api/src/org/apache/cloudstack/api/command/admin/ca/ListCAProvidersCmd.java new file mode 100644 index 000000000000..e1e8e3751633 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/ca/ListCAProvidersCmd.java @@ -0,0 +1,102 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.ca; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.CAProviderResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.framework.ca.CAProvider; + +import com.cloud.user.Account; + +@APICommand(name = ListCAProvidersCmd.APINAME, + description = "Lists available certificate authority providers in CloudStack", + responseObject = CAProviderResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.11.0", + authorized = {RoleType.Admin}) +public class ListCAProvidersCmd extends BaseCmd { + public static final String APINAME = "listCAProviders"; + + @Inject + private CAManager caManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "List CA service provider by name") + private String name; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getName() { + return name; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + private void setupResponse(final List providers) { + final ListResponse response = new ListResponse<>(); + final List responses = new ArrayList<>(); + for (final CAProvider provider : providers) { + if (provider == null || (getName() != null && !provider.getProviderName().equals(getName()))) { + continue; + } + final CAProviderResponse caProviderResponse = new CAProviderResponse(); + caProviderResponse.setName(provider.getProviderName()); + caProviderResponse.setDescription(provider.getDescription()); + caProviderResponse.setObjectName("caprovider"); + responses.add(caProviderResponse); + } + response.setResponses(responses); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public void execute() { + final List caProviders = caManager.getCaProviders(); + setupResponse(caProviders); + } +} diff --git a/api/src/org/apache/cloudstack/api/command/admin/ca/ListCaCertificateCmd.java b/api/src/org/apache/cloudstack/api/command/admin/ca/ListCaCertificateCmd.java new file mode 100644 index 000000000000..1baa84179f03 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/ca/ListCaCertificateCmd.java @@ -0,0 +1,90 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.ca; + +import java.io.IOException; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.CertificateResponse; +import org.apache.cloudstack.ca.CAManager; + +import com.cloud.user.Account; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = ListCaCertificateCmd.APINAME, + description = "Lists the CA public certificate(s) as support by the configured/provided CA plugin", + responseObject = CertificateResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.11.0", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListCaCertificateCmd extends BaseCmd { + public static final String APINAME = "listCaCertificate"; + + @Inject + private CAManager caManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, description = "Name of the CA service provider, otherwise the default configured provider plugin will be used") + private String provider; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getProvider() { + return provider; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + final String caCertificates; + try { + caCertificates = caManager.getCaCertificate(getProvider()); + } catch (final IOException e) { + throw new CloudRuntimeException("Failed to get CA certificates for given CA provider"); + } + final CertificateResponse certificateResponse = new CertificateResponse("cacertificates"); + certificateResponse.setCertificate(caCertificates); + certificateResponse.setResponseName(getCommandName()); + setResponseObject(certificateResponse); + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_TYPE_NORMAL; + } +} diff --git a/api/src/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java b/api/src/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java new file mode 100644 index 000000000000..2745f071dd07 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/ca/ProvisionCertificateCmd.java @@ -0,0 +1,125 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.ca; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandJobType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.cloud.host.Host; + +@APICommand(name = ProvisionCertificateCmd.APINAME, + description = "Issues and propagates client certificate on a connected host/agent using configured CA plugin", + responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + since = "4.11.0", + authorized = {RoleType.Admin}) +public class ProvisionCertificateCmd extends BaseAsyncCmd { + public static final String APINAME = "provisionCertificate"; + + @Inject + private CAManager caManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, required = true, entityType = HostResponse.class, + description = "The host/agent uuid to which the certificate has to be provisioned (issued and propagated)") + private Long hostId; + + @Parameter(name = ApiConstants.RECONNECT, type = CommandType.BOOLEAN, + description = "Whether to attempt reconnection with host/agent after successful deployment of certificate. When option is not provided, configured global setting is used") + private Boolean reconnect; + + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, + description = "Name of the CA service provider, otherwise the default configured provider plugin will be used") + private String provider; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getHostId() { + return hostId; + } + + public Boolean getReconnect() { + return reconnect; + } + + public String getProvider() { + return provider; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + final Host host = _resourceService.getHost(getHostId()); + if (host == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to find host by ID: " + getHostId()); + } + + boolean result = caManager.provisionCertificate(host, getReconnect(), getProvider()); + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setSuccess(result); + setResponseObject(response); + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_CA_CERTIFICATE_PROVISION; + } + + @Override + public String getEventDescription() { + return "provisioning certificate for host id=" + hostId + " using provider=" + provider; + } + + @Override + public ApiCommandJobType getInstanceType() { + return ApiCommandJobType.Host; + } +} diff --git a/api/src/org/apache/cloudstack/api/command/admin/ca/RevokeCertificateCmd.java b/api/src/org/apache/cloudstack/api/command/admin/ca/RevokeCertificateCmd.java new file mode 100644 index 000000000000..0f154f045df0 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/ca/RevokeCertificateCmd.java @@ -0,0 +1,116 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.command.admin.ca; + +import java.math.BigInteger; + +import javax.inject.Inject; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseAsyncCmd; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.event.EventTypes; +import com.google.common.base.Strings; + +@APICommand(name = RevokeCertificateCmd.APINAME, + description = "Revokes certificate using configured CA plugin", + responseObject = SuccessResponse.class, + requestHasSensitiveInfo = true, + responseHasSensitiveInfo = false, + since = "4.11.0", + authorized = {RoleType.Admin}) +public class RevokeCertificateCmd extends BaseAsyncCmd { + + public static final String APINAME = "revokeCertificate"; + + @Inject + private CAManager caManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.SERIAL, type = BaseCmd.CommandType.STRING, required = true, description = "The certificate serial number, as a hex value") + private String serial; + + @Parameter(name = ApiConstants.CN, type = BaseCmd.CommandType.STRING, description = "The certificate CN") + private String cn; + + @Parameter(name = ApiConstants.PROVIDER, type = BaseCmd.CommandType.STRING, description = "Name of the CA service provider, otherwise the default configured provider plugin will be used") + private String provider; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public BigInteger getSerialBigInteger() { + if (Strings.isNullOrEmpty(serial)) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Certificate serial cannot be empty"); + } + return new BigInteger(serial, 16); + } + + public String getCn() { + return cn; + } + + public String getProvider() { + return provider; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + boolean result = caManager.revokeCertificate(getSerialBigInteger(), getCn(), getProvider()); + SuccessResponse response = new SuccessResponse(getCommandName()); + response.setSuccess(result); + setResponseObject(response); + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public String getEventType() { + return EventTypes.EVENT_CA_CERTIFICATE_REVOKE; + } + + @Override + public String getEventDescription() { + return "revoking certificate with serial id=" + serial + ", cn=" + cn; + } +} diff --git a/api/src/org/apache/cloudstack/api/response/CAProviderResponse.java b/api/src/org/apache/cloudstack/api/response/CAProviderResponse.java new file mode 100644 index 000000000000..94d5882e18ac --- /dev/null +++ b/api/src/org/apache/cloudstack/api/response/CAProviderResponse.java @@ -0,0 +1,52 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.framework.ca.CAProvider; + +@EntityReference(value = CAProvider.class) +public class CAProviderResponse extends BaseResponse { + @SerializedName(ApiConstants.NAME) + @Param(description = "the CA service provider name") + private String name; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "the description of the CA service provider") + private String description; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/api/src/org/apache/cloudstack/api/response/CertificateResponse.java b/api/src/org/apache/cloudstack/api/response/CertificateResponse.java new file mode 100644 index 000000000000..f8c3ecc74044 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/response/CertificateResponse.java @@ -0,0 +1,58 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.api.response; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class CertificateResponse extends BaseResponse { + @SerializedName(ApiConstants.CERTIFICATE) + @Param(description = "The client certificate") + private String certificate = ""; + + @SerializedName(ApiConstants.PRIVATE_KEY) + @Param(description = "Private key for the certificate") + private String privateKey; + + @SerializedName(ApiConstants.CA_CERTIFICATES) + @Param(description = "The CA certificate(s)") + private String caCertificate; + + public CertificateResponse() { + setObjectName("certificates"); + } + + public CertificateResponse(final String objectName) { + setObjectName(objectName); + } + + public void setCertificate(final String certificate) { + this.certificate = certificate; + } + + public void setPrivateKey(final String privateKey) { + this.privateKey = privateKey; + } + + public void setCaCertificate(final String caCertificate) { + this.caCertificate = caCertificate; + } +} diff --git a/api/src/org/apache/cloudstack/ca/CAManager.java b/api/src/org/apache/cloudstack/ca/CAManager.java new file mode 100644 index 000000000000..c32cfbfe3455 --- /dev/null +++ b/api/src/org/apache/cloudstack/ca/CAManager.java @@ -0,0 +1,163 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.ca; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; + +import org.apache.cloudstack.framework.ca.CAProvider; +import org.apache.cloudstack.framework.ca.CAService; +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; + +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.Host; +import com.cloud.utils.component.PluggableService; + +public interface CAManager extends CAService, Configurable, PluggableService { + + ConfigKey CAProviderPlugin = new ConfigKey<>("Advanced", String.class, + "ca.framework.provider.plugin", + "root", + "The CA provider plugin that is used for secure CloudStack management server-agent communication for encryption and authentication. Restart management server(s) when changed.", true); + + ConfigKey CertKeySize = new ConfigKey<>("Advanced", Integer.class, + "ca.framework.cert.keysize", + "2048", + "The key size to be used for random certificate keypair generation.", true); + + ConfigKey CertSignatureAlgorithm = new ConfigKey<>("Advanced", String.class, + "ca.framework.cert.signature.algorithm", + "SHA256withRSA", + "The default signature algorithm to use for certificate generation.", true); + + + ConfigKey CertValidityPeriod = new ConfigKey<>("Advanced", Integer.class, + "ca.framework.cert.validity.period", + "365", + "The validity period of a client certificate in number of days. Set the value to be more than the expiry alert period.", true); + + ConfigKey AutomaticCertRenewal = new ConfigKey<>("Advanced", Boolean.class, + "ca.framework.cert.automatic.renewal", + "true", + "Enable automatic renewal and provisioning of certificate to agents as supported by the configured CA plugin.", true, ConfigKey.Scope.Cluster); + + ConfigKey CABackgroundJobDelay = new ConfigKey<>("Advanced", Long.class, + "ca.framework.background.task.delay", + "3600", + "The CA framework background task delay in seconds. Background task runs expiry checks and renews certificate if auto-renewal is enabled.", true); + + ConfigKey CertExpiryAlertPeriod = new ConfigKey<>("Advanced", Integer.class, + "ca.framework.cert.expiry.alert.period", + "15", + "The number of days before expiry of a client certificate, the validations are checked. Admins are alerted when auto-renewal is not allowed, otherwise auto-renewal is attempted.", true, ConfigKey.Scope.Cluster); + + /** + * Returns a list of available CA provider plugins + * @return returns list of CAProvider + */ + List getCaProviders(); + + /** + * Returns a map of active agents/hosts certificates + * @return returns a non-null map + */ + Map getActiveCertificatesMap(); + + /** + * Checks whether the configured CA plugin can provision/create certificates + * @return returns certificate creation capability + */ + boolean canProvisionCertificates(); + + /** + * Returns PEM-encoded chained CA certificate + * @param caProvider + * @return returns CA certificate chain string + */ + String getCaCertificate(final String caProvider) throws IOException; + + /** + * Issues client Certificate + * @param csr + * @param ipAddresses + * @param domainNames + * @param validityDays + * @param provider + * @return returns Certificate + */ + Certificate issueCertificate(final String csr, final List domainNames, final List ipAddresses, final Integer validityDays, final String provider); + + /** + * Revokes certificate from provided serial and CN + * @param certSerial + * @param certCn + * @return returns success/failure as boolean + */ + boolean revokeCertificate(final BigInteger certSerial, final String certCn, final String provider); + + /** + * Provisions certificate for given active and connected agent host + * @param host + * @param provider + * @return returns success/failure as boolean + */ + boolean provisionCertificate(final Host host, final Boolean reconnect, final String provider); + + /** + * Setups up a new keystore and generates CSR for a host + * @param host + * @param sshAccessDetails when provided, VirtualRoutingResource uses router proxy to execute commands via SSH in systemvms + * @return + * @throws AgentUnavailableException + * @throws OperationTimedoutException + */ + String generateKeyStoreAndCsr(final Host host, final Map sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException; + + /** + * Deploys a Certificate payload to a provided host + * @param host + * @param certificate + * @param reconnect when true the host/agent is reconnected on successful deployment of the certificate + * @param sshAccessDetails when provided, VirtualRoutingResource uses router proxy to execute commands via SSH in systemvms + * @return + * @throws AgentUnavailableException + * @throws OperationTimedoutException + */ + boolean deployCertificate(final Host host, final Certificate certificate, final Boolean reconnect, final Map sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException; + + /** + * Removes the host from an internal active client/certificate map + * @param host + */ + void purgeHostCertificate(final Host host); + + /** + * Sends a CA cert event alert to admins with a subject and a message + * @param host + * @param subject + * @param message + */ + void sendAlert(final Host host, final String subject, final String message); + +} diff --git a/api/src/org/apache/cloudstack/poll/BackgroundPollTask.java b/api/src/org/apache/cloudstack/poll/BackgroundPollTask.java index 8eea147955b4..5f1b3300c483 100644 --- a/api/src/org/apache/cloudstack/poll/BackgroundPollTask.java +++ b/api/src/org/apache/cloudstack/poll/BackgroundPollTask.java @@ -18,4 +18,10 @@ package org.apache.cloudstack.poll; public interface BackgroundPollTask extends Runnable { + /** + * Returns delay in milliseconds between two rounds + * When it returns null a default value is used + * @return + */ + Long getDelay(); } diff --git a/client/pom.xml b/client/pom.xml index 9b8fa3866ee0..7af4324c5791 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -63,6 +63,11 @@ cloud-plugin-acl-dynamic-role-based ${project.version} + + org.apache.cloudstack + cloud-plugin-ca-rootca + ${project.version} + org.apache.cloudstack cloud-plugin-dedicated-resources @@ -274,6 +279,11 @@ cloud-mom-kafka ${project.version} + + org.apache.cloudstack + cloud-framework-ca + ${project.version} + org.apache.cloudstack cloud-framework-ipc @@ -478,6 +488,7 @@ /client ${project.build.directory}/utilities/scripts/db/;${project.build.directory}/utilities/scripts/db/db/ + .*/cloud.*jar$|.*/classes/.* diff --git a/client/tomcatconf/cloudmanagementserver.keystore b/client/tomcatconf/cloudmanagementserver.keystore deleted file mode 100644 index 3ee4d13565afb330f20506a01b35ee4a126ddcae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1316 zcmezO_TO6u1_mY|W&~r_+{*0KN+8cj?ar4=K$+bJO-$Pj_}I9#*%(Po#$|r-+OPDsr;+C4RU!|qHGM$SGCL$p6b6Q-Fk&hWRa#ya`9%7dA7-$y2@5% zi>F&JEVy+t_*X2e_cjyR6z$MM+aJGP!0`On_jj+;wm%i!zLT+Mm05<5bIm&69k)B* zpDk+&S|oa;RHtlC^CzY4N>S}!!biofrOigT=7? z?WNA1qb0J!>wjkdbn;oAw)kg*`sZ2q%0wnDwYJK78e602#q{d8_gmBV z#x8gNm6oxMb!X6(D<$!><}4PEwqR48{+-crz4YPeq6=w3dqR8j_@32$E$9`zX*M&Z zZ<3lA+d}5RS(hgU7Huk+x@t{Ke!=r;s?ItQk5=lu`YiTm+Ir_GhoHd9$ivL~b2U?} zKAly0eEDY4rTvwjb2g-XGdMrLvc=}3#TG}MH?Ow!OFmbssCw`#V6OAyxb0`xeyE>e z$+WRYEW&ze&9qOy{yl4odZ+ejg|p$j+@ObAa?SK;_eU@)w+1^F-Io3r(fj;ZH|KsUCGiJpE$X?6cXOJ0_Gtw+ z-Bfw>)G0w)FKO9V-5cjWUpT{Z{Nk5#PDXD9_p>KLEM~lDGM~Jh?WC&8<+_*cvCB5H zF3=NB%QpG7ZO61(->>gcyuoPgw(*yS`onZpi=be8}^>L`%PoV5BBKIF?ug|Wf(E$gtBxkVm|tGwVrv@ zm%K{JzxQlT&WX7`d$F!olWZB!z2f$Qwr}{!6e#K^?N;urs&-GG;kQ>)FR?K>|pS+g=2gcu3{(>ik~3!5-|XkK<+etDju zkbwY5giDyyIVZm~MK3u&7beDyEGA(f22#f?%nw!+sTYh-9(VPpyA(#Y1v`N+Nl=33^)UIv54PNv32hWpXw=MS#4NIQ10ijB(~{IX?yEHBY!M2{)Y0{~z?9C)YNmXy+z|ySwDMq6HslMg4lm#=i1{ zq4ysxHn)S1rvG@iwEU6Ck?f?d&pC-Fdp|e*P*jWGxheQhK&8->4u*-my*0cBtLNW+ ix;f($pNUJBma94=|B@SX<`!+6@^U4!V)msE;YR_>&@g=f diff --git a/client/tomcatconf/server-ssl.xml.in b/client/tomcatconf/server-ssl.xml.in index 595879fbfd10..8368a65c3bc5 100755 --- a/client/tomcatconf/server-ssl.xml.in +++ b/client/tomcatconf/server-ssl.xml.in @@ -94,7 +94,7 @@ maxThreads="150" scheme="https" secure="true" URIEncoding="UTF-8" clientAuth="false" sslProtocol="TLS" keystoreType="JKS" - keystoreFile="/etc/cloudstack/management/cloudmanagementserver.keystore" + keystoreFile="/etc/cloudstack/management/cloud.jks" keystorePass="vmops.com"/> diff --git a/client/tomcatconf/server7-ssl.xml.in b/client/tomcatconf/server7-ssl.xml.in index 2633bcad7601..d4fe899b57a4 100755 --- a/client/tomcatconf/server7-ssl.xml.in +++ b/client/tomcatconf/server7-ssl.xml.in @@ -94,7 +94,7 @@ maxThreads="150" scheme="https" secure="true" URIEncoding="UTF-8" clientAuth="false" sslProtocol="TLS" keystoreType="JKS" - keystoreFile="/etc/cloudstack/management/cloudmanagementserver.keystore" + keystoreFile="/etc/cloudstack/management/cloud.jks" keystorePass="vmops.com"/> diff --git a/client/tomcatconf/tomcat6-ssl.conf.in b/client/tomcatconf/tomcat6-ssl.conf.in index e7c53ac9f8f4..1d6f59b0787d 100644 --- a/client/tomcatconf/tomcat6-ssl.conf.in +++ b/client/tomcatconf/tomcat6-ssl.conf.in @@ -40,7 +40,7 @@ CATALINA_TMPDIR="@MSENVIRON@/temp" # Use JAVA_OPTS to set java.library.path for libtcnative.so #JAVA_OPTS="-Djava.library.path=/usr/lib64" -JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Djavax.net.ssl.trustStore=/etc/cloudstack/management/cloudmanagementserver.keystore -Djavax.net.ssl.trustStorePassword=vmops.com -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=@MSLOGDIR@ -XX:MaxPermSize=800m -XX:PermSize=512M -Djava.security.properties=/etc/cloudstack/management/java.security.ciphers" +JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Djavax.net.ssl.trustStore=/etc/cloudstack/management/cloud.jks -Djavax.net.ssl.trustStorePassword=vmops.com -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=@MSLOGDIR@ -XX:MaxPermSize=800m -XX:PermSize=512M -Djava.security.properties=/etc/cloudstack/management/java.security.ciphers" # What user should run tomcat TOMCAT_USER="@MSUSER@" diff --git a/core/resources/META-INF/cloudstack/ca/module.properties b/core/resources/META-INF/cloudstack/ca/module.properties new file mode 100644 index 000000000000..1a6915aea90e --- /dev/null +++ b/core/resources/META-INF/cloudstack/ca/module.properties @@ -0,0 +1,21 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +name=ca +parent=backend diff --git a/core/resources/META-INF/cloudstack/ca/spring-core-lifecycle-ca-context-inheritable.xml b/core/resources/META-INF/cloudstack/ca/spring-core-lifecycle-ca-context-inheritable.xml new file mode 100644 index 000000000000..1566a4b076b9 --- /dev/null +++ b/core/resources/META-INF/cloudstack/ca/spring-core-lifecycle-ca-context-inheritable.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml index a9124d37ac1a..d5b912ac8929 100644 --- a/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml +++ b/core/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml @@ -312,4 +312,8 @@ + + + diff --git a/core/src/com/cloud/agent/api/routing/NetworkElementCommand.java b/core/src/com/cloud/agent/api/routing/NetworkElementCommand.java index a7b5b5e6d17b..ae482ac71ec7 100644 --- a/core/src/com/cloud/agent/api/routing/NetworkElementCommand.java +++ b/core/src/com/cloud/agent/api/routing/NetworkElementCommand.java @@ -20,6 +20,7 @@ package com.cloud.agent.api.routing; import java.util.HashMap; +import java.util.Map; import com.cloud.agent.api.Command; @@ -46,6 +47,18 @@ protected NetworkElementCommand() { super(); } + public void setAccessDetail(final Map details) { + if (details == null) { + return; + } + for (final Map.Entry detail : details.entrySet()) { + if (detail == null) { + continue; + } + setAccessDetail(detail.getKey(), detail.getValue()); + } + } + public void setAccessDetail(final String name, final String value) { accessDetails.put(name, value); } diff --git a/core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java b/core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java index 75f55f949a29..d7c3d56f9b9b 100644 --- a/core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java +++ b/core/src/com/cloud/agent/resource/virtualnetwork/VirtualRoutingResource.java @@ -35,6 +35,11 @@ import javax.naming.ConfigurationException; +import org.apache.cloudstack.ca.SetupCertificateAnswer; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.ca.SetupKeyStoreCommand; +import org.apache.cloudstack.ca.SetupKeystoreAnswer; +import org.apache.cloudstack.utils.security.KeyStoreUtils; import org.apache.log4j.Logger; import com.cloud.agent.api.Answer; @@ -108,6 +113,14 @@ public Answer executeRequest(final NetworkElementCommand cmd) { return executeQueryCommand(cmd); } + if (cmd instanceof SetupKeyStoreCommand) { + return execute((SetupKeyStoreCommand) cmd); + } + + if (cmd instanceof SetupCertificateCommand) { + return execute((SetupCertificateCommand) cmd); + } + if (cmd instanceof AggregationControlCommand) { return execute((AggregationControlCommand)cmd); } @@ -139,6 +152,37 @@ public Answer executeRequest(final NetworkElementCommand cmd) { } } + private Answer execute(final SetupKeyStoreCommand cmd) { + final String args = String.format("/usr/local/cloud/systemvm/conf/agent.properties " + + "/usr/local/cloud/systemvm/conf/%s " + + "%s %d " + + "/usr/local/cloud/systemvm/conf/%s", + KeyStoreUtils.defaultKeystoreFile, + cmd.getKeystorePassword(), + cmd.getValidityDays(), + KeyStoreUtils.defaultCsrFile); + ExecutionResult result = _vrDeployer.executeInVR(cmd.getRouterAccessIp(), KeyStoreUtils.keyStoreSetupScript, args); + return new SetupKeystoreAnswer(result.getDetails()); + } + + private Answer execute(final SetupCertificateCommand cmd) { + final String args = String.format("/usr/local/cloud/systemvm/conf/agent.properties " + + "/usr/local/cloud/systemvm/conf/%s %s " + + "/usr/local/cloud/systemvm/conf/%s \"%s\" " + + "/usr/local/cloud/systemvm/conf/%s \"%s\" " + + "/usr/local/cloud/systemvm/conf/%s \"%s\"", + KeyStoreUtils.defaultKeystoreFile, + KeyStoreUtils.sshMode, + KeyStoreUtils.defaultCertFile, + cmd.getEncodedCertificate(), + KeyStoreUtils.defaultCaCertFile, + cmd.getEncodedCaCertificates(), + KeyStoreUtils.defaultPrivateKeyFile, + cmd.getEncodedPrivateKey()); + ExecutionResult result = _vrDeployer.executeInVR(cmd.getRouterAccessIp(), KeyStoreUtils.keyStoreImportScript, args); + return new SetupCertificateAnswer(result.isSuccess()); + } + private Answer executeQueryCommand(NetworkElementCommand cmd) { if (cmd instanceof CheckRouterCommand) { return execute((CheckRouterCommand)cmd); diff --git a/core/src/org/apache/cloudstack/ca/SetupCertificateAnswer.java b/core/src/org/apache/cloudstack/ca/SetupCertificateAnswer.java new file mode 100644 index 000000000000..4df5d158da9f --- /dev/null +++ b/core/src/org/apache/cloudstack/ca/SetupCertificateAnswer.java @@ -0,0 +1,29 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca; + +import com.cloud.agent.api.Answer; + +public class SetupCertificateAnswer extends Answer { + public SetupCertificateAnswer(final boolean result) { + super(null); + this.result = result; + } +} diff --git a/core/src/org/apache/cloudstack/ca/SetupCertificateCommand.java b/core/src/org/apache/cloudstack/ca/SetupCertificateCommand.java new file mode 100644 index 000000000000..1cd31509d392 --- /dev/null +++ b/core/src/org/apache/cloudstack/ca/SetupCertificateCommand.java @@ -0,0 +1,99 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca; + +import java.io.IOException; +import java.util.Map; + +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.utils.security.CertUtils; +import org.apache.cloudstack.utils.security.KeyStoreUtils; + +import com.cloud.agent.api.LogLevel; +import com.cloud.agent.api.routing.NetworkElementCommand; +import com.cloud.utils.exception.CloudRuntimeException; + +public class SetupCertificateCommand extends NetworkElementCommand { + @LogLevel(LogLevel.Log4jLevel.Off) + private String certificate; + @LogLevel(LogLevel.Log4jLevel.Off) + private String privateKey = ""; + @LogLevel(LogLevel.Log4jLevel.Off) + private String caCertificates; + + private boolean handleByAgent = true; + + public SetupCertificateCommand(final Certificate certificate) { + super(); + if (certificate == null) { + throw new CloudRuntimeException("A null certificate was provided to setup"); + } + setWait(60); + try { + this.certificate = CertUtils.x509CertificateToPem(certificate.getClientCertificate()); + this.caCertificates = CertUtils.x509CertificatesToPem(certificate.getCaCertificates()); + if (certificate.getPrivateKey() != null) { + this.privateKey = CertUtils.privateKeyToPem(certificate.getPrivateKey()); + } + } catch (final IOException e) { + throw new CloudRuntimeException("Failed to transform X509 cert to PEM format", e); + } + } + + @Override + public void setAccessDetail(final Map accessDetails) { + handleByAgent = false; + super.setAccessDetail(accessDetails); + } + + @Override + public void setAccessDetail(String name, String value) { + handleByAgent = false; + super.setAccessDetail(name, value); + } + + public String getPrivateKey() { + return privateKey; + } + + public String getCertificate() { + return certificate; + } + + public String getCaCertificates() { + return caCertificates; + } + + public String getEncodedPrivateKey() { + return privateKey.replace("\n", KeyStoreUtils.certNewlineEncoder).replace(" ", KeyStoreUtils.certSpaceEncoder); + } + + public String getEncodedCertificate() { + return certificate.replace("\n", KeyStoreUtils.certNewlineEncoder).replace(" ", KeyStoreUtils.certSpaceEncoder); + } + + public String getEncodedCaCertificates() { + return caCertificates.replace("\n", KeyStoreUtils.certNewlineEncoder).replace(" ", KeyStoreUtils.certSpaceEncoder); + } + + public boolean isHandleByAgent() { + return handleByAgent; + } +} diff --git a/core/src/org/apache/cloudstack/ca/SetupKeyStoreCommand.java b/core/src/org/apache/cloudstack/ca/SetupKeyStoreCommand.java new file mode 100644 index 000000000000..7cd5cbee20fc --- /dev/null +++ b/core/src/org/apache/cloudstack/ca/SetupKeyStoreCommand.java @@ -0,0 +1,75 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca; + +import java.util.Map; + +import com.cloud.agent.api.LogLevel; +import com.cloud.agent.api.routing.NetworkElementCommand; +import com.cloud.utils.PasswordGenerator; + +public class SetupKeyStoreCommand extends NetworkElementCommand { + @LogLevel(LogLevel.Log4jLevel.Off) + private int validityDays; + @LogLevel(LogLevel.Log4jLevel.Off) + private String keystorePassword; + + private boolean handleByAgent = true; + + public SetupKeyStoreCommand(final int validityDays) { + super(); + setWait(60); + this.validityDays = validityDays; + if (this.validityDays < 1) { + this.validityDays = 1; + } + this.keystorePassword = PasswordGenerator.generateRandomPassword(16); + } + + @Override + public void setAccessDetail(final Map accessDetails) { + handleByAgent = false; + super.setAccessDetail(accessDetails); + } + + + @Override + public void setAccessDetail(String name, String value) { + handleByAgent = false; + super.setAccessDetail(name, value); + } + + public int getValidityDays() { + return validityDays; + } + + public String getKeystorePassword() { + return keystorePassword; + } + + public boolean isHandleByAgent() { + return handleByAgent; + } + + @Override + public boolean executeInSequence() { + return false; + } +} diff --git a/core/src/org/apache/cloudstack/ca/SetupKeystoreAnswer.java b/core/src/org/apache/cloudstack/ca/SetupKeystoreAnswer.java new file mode 100644 index 000000000000..16ddc96c5ea1 --- /dev/null +++ b/core/src/org/apache/cloudstack/ca/SetupKeystoreAnswer.java @@ -0,0 +1,37 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca; + +import com.cloud.agent.api.LogLevel; +import com.google.common.base.Strings; + +public class SetupKeystoreAnswer extends SetupCertificateAnswer { + @LogLevel(LogLevel.Log4jLevel.Off) + private final String csr; + + public SetupKeystoreAnswer(final String csr) { + super(!Strings.isNullOrEmpty(csr)); + this.csr = csr; + } + + public String getCsr() { + return csr; + } +} diff --git a/debian/cloudstack-management.postinst b/debian/cloudstack-management.postinst index 240224d9252e..5c9a7fa8ceb9 100644 --- a/debian/cloudstack-management.postinst +++ b/debian/cloudstack-management.postinst @@ -50,9 +50,6 @@ if [ "$1" = configure ]; then cp -a $OLDCONFDIR/$FILE $NEWCONFDIR/$FILE fi done - if [ -f "$OLDCONFDIR/cloud.keystore" ]; then - cp -a $OLDCONFDIR/cloud.keystore $NEWCONFDIR/cloudmanagementserver.keystore - fi fi CONFDIR="/etc/cloudstack/management" diff --git a/developer/developer-prefill.sql b/developer/developer-prefill.sql index 7fd85c615531..cc67748b8dd9 100644 --- a/developer/developer-prefill.sql +++ b/developer/developer-prefill.sql @@ -119,6 +119,11 @@ INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) VALUES ('Advanced', 'DEFAULT', 'RoleService', 'dynamic.apichecker.enabled', 'true'); +-- Enable RootCA auth strictness for fresh deployments +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) + VALUES ('Advanced', 'DEFAULT', 'RootCAProvider', + 'ca.plugin.root.auth.strictness', 'true'); + -- Add developer configuration entry; allows management server to be run as a user other than "cloud" INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) VALUES ('Advanced', 'DEFAULT', 'management-server', diff --git a/engine/api/src/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java b/engine/api/src/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java index 13bfee6ea27d..68531e3ab3d5 100644 --- a/engine/api/src/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java +++ b/engine/api/src/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java @@ -98,6 +98,8 @@ void prepare(VirtualMachineProfile profile, DeployDestination dest, ReservationC List getNicProfiles(VirtualMachine vm); + Map getSystemVMAccessDetails(VirtualMachine vm); + Pair implementNetwork(long networkId, DeployDestination dest, ReservationContext context) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException; diff --git a/engine/orchestration/src/com/cloud/agent/manager/AgentManagerImpl.java b/engine/orchestration/src/com/cloud/agent/manager/AgentManagerImpl.java index 597ea6733e9f..325f3ec739f9 100644 --- a/engine/orchestration/src/com/cloud/agent/manager/AgentManagerImpl.java +++ b/engine/orchestration/src/com/cloud/agent/manager/AgentManagerImpl.java @@ -37,6 +37,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; @@ -134,6 +135,8 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl protected int _monitorId = 0; private final Lock _agentStatusLock = new ReentrantLock(); + @Inject + protected CAManager caService; @Inject protected EntityManager _entityMgr; @@ -223,7 +226,7 @@ public boolean configure(final String name, final Map params) th // allow core threads to time out even when there are no items in the queue _connectExecutor.allowCoreThreadTimeOut(true); - _connection = new NioServer("AgentManager", Port.value(), Workers.value() + 10, this); + _connection = new NioServer("AgentManager", Port.value(), Workers.value() + 10, this, caService); s_logger.info("Listening on " + Port.value() + " with " + Workers.value() + " workers"); // executes all agent commands other than cron and ping @@ -813,6 +816,7 @@ protected boolean handleDisconnectWithoutInvestigation(final AgentAttache attach s_logger.debug("The next status of agent " + hostId + "is " + nextStatus + ", current status is " + currentStatus); } } + caService.purgeHostCertificate(host); } if (s_logger.isDebugEnabled()) { diff --git a/engine/orchestration/src/com/cloud/agent/manager/ClusteredAgentManagerImpl.java b/engine/orchestration/src/com/cloud/agent/manager/ClusteredAgentManagerImpl.java index b69ca5bb94df..faf3a3bfcd4d 100644 --- a/engine/orchestration/src/com/cloud/agent/manager/ClusteredAgentManagerImpl.java +++ b/engine/orchestration/src/com/cloud/agent/manager/ClusteredAgentManagerImpl.java @@ -495,6 +495,7 @@ public SocketChannel connectToPeer(final String peerName, final SocketChannel pr } final String ip = ms.getServiceIP(); InetAddress addr; + int port = Port.value(); try { addr = InetAddress.getByName(ip); } catch (final UnknownHostException e) { @@ -502,21 +503,21 @@ public SocketChannel connectToPeer(final String peerName, final SocketChannel pr } SocketChannel ch1 = null; try { - ch1 = SocketChannel.open(new InetSocketAddress(addr, Port.value())); + ch1 = SocketChannel.open(new InetSocketAddress(addr, port)); ch1.configureBlocking(false); ch1.socket().setKeepAlive(true); ch1.socket().setSoTimeout(60 * 1000); try { - final SSLContext sslContext = Link.initSSLContext(true); - sslEngine = sslContext.createSSLEngine(ip, Port.value()); + SSLContext sslContext = Link.initClientSSLContext(); + sslEngine = sslContext.createSSLEngine(ip, port); sslEngine.setUseClientMode(true); sslEngine.setEnabledProtocols(SSLUtils.getSupportedProtocols(sslEngine.getEnabledProtocols())); sslEngine.beginHandshake(); if (!Link.doHandshake(ch1, sslEngine, true)) { ch1.close(); - throw new IOException("SSL handshake failed!"); + throw new IOException(String.format("SSL: Handshake failed with peer management server '%s' on %s:%d ", peerName, ip, port)); } - s_logger.info("SSL: Handshake done"); + s_logger.info(String.format("SSL: Handshake done with peer management server '%s' on %s:%d ", peerName, ip, port)); } catch (final Exception e) { ch1.close(); throw new IOException("SSL: Fail to init SSL! " + e); @@ -528,10 +529,12 @@ public SocketChannel connectToPeer(final String peerName, final SocketChannel pr _sslEngines.put(peerName, sslEngine); return ch1; } catch (final IOException e) { - try { - ch1.close(); - } catch (final IOException ex) { - s_logger.error("failed to close failed peer socket: " + ex); + if (ch1 != null) { + try { + ch1.close(); + } catch (final IOException ex) { + s_logger.error("failed to close failed peer socket: " + ex); + } } s_logger.warn("Unable to connect to peer management server: " + peerName + ", ip: " + ip + " due to " + e.getMessage(), e); return null; diff --git a/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java b/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java index babfbdb54cb8..638a00047e8c 100755 --- a/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/engine/orchestration/src/com/cloud/vm/VirtualMachineManagerImpl.java @@ -22,6 +22,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; @@ -39,12 +40,14 @@ import com.cloud.agent.api.AttachOrDettachConfigDriveCommand; import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; +import org.apache.cloudstack.ca.CAManager; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; import org.apache.cloudstack.engine.subsystem.api.storage.StoragePoolAllocator; +import org.apache.cloudstack.framework.ca.Certificate; import org.apache.cloudstack.framework.config.ConfigDepot; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; @@ -97,6 +100,7 @@ import com.cloud.agent.api.UnPlugNicAnswer; import com.cloud.agent.api.UnPlugNicCommand; import com.cloud.agent.api.UnregisterVMCommand; +import com.cloud.agent.api.routing.NetworkElementCommand; import com.cloud.agent.api.to.DiskTO; import com.cloud.agent.api.to.GPUDeviceTO; import com.cloud.agent.api.to.NicTO; @@ -205,6 +209,7 @@ import com.cloud.vm.snapshot.VMSnapshotManager; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import com.google.common.base.Strings; public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMachineManager, VmWorkJobHandler, Listener, Configurable { private static final Logger s_logger = Logger.getLogger(VirtualMachineManagerImpl.class); @@ -284,7 +289,9 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac @Inject protected UserVmDetailsDao _vmDetailsDao; @Inject - ServiceOfferingDao _serviceOfferingDao = null; + protected ServiceOfferingDao _serviceOfferingDao = null; + @Inject + protected CAManager caManager; @Inject ConfigDepot _configDepot; @@ -1023,7 +1030,6 @@ public void orchestrateStart(final String vmUuid, final Map sshAccessDetails = _networkMgr.getSystemVMAccessDetails(vm); + final String csr = caManager.generateKeyStoreAndCsr(vmHost, sshAccessDetails); + if (!Strings.isNullOrEmpty(csr)) { + final Map ipAddressDetails = new HashMap<>(sshAccessDetails); + ipAddressDetails.remove(NetworkElementCommand.ROUTER_NAME); + final Certificate certificate = caManager.issueCertificate(csr, Arrays.asList(vm.getHostName(), vm.getInstanceName()), new ArrayList<>(ipAddressDetails.values()), CAManager.CertValidityPeriod.value(), null); + final boolean result = caManager.deployCertificate(vmHost, certificate, false, sshAccessDetails); + if (!result) { + s_logger.error("Failed to setup certificate for system vm: " + vm.getInstanceName()); + } + } else { + s_logger.error("Failed to setup keystore and generate CSR for system vm: " + vm.getInstanceName()); + } + } return; } else { if (s_logger.isDebugEnabled()) { diff --git a/engine/orchestration/src/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index 37f53302455a..3956617859c1 100644 --- a/engine/orchestration/src/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -68,6 +68,7 @@ import com.cloud.agent.api.Command; import com.cloud.agent.api.StartupCommand; import com.cloud.agent.api.StartupRoutingCommand; +import com.cloud.agent.api.routing.NetworkElementCommand; import com.cloud.agent.api.to.NicTO; import com.cloud.alert.AlertManager; import com.cloud.configuration.ConfigurationManager; @@ -215,6 +216,7 @@ import com.cloud.vm.dao.NicSecondaryIpVO; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDao; +import com.google.common.base.Strings; /** * NetworkManagerImpl implements NetworkManager. @@ -3488,6 +3490,39 @@ public List getNicProfiles(final VirtualMachine vm) { return profiles; } + @Override + public Map getSystemVMAccessDetails(final VirtualMachine vm) { + final Map accessDetails = new HashMap<>(); + accessDetails.put(NetworkElementCommand.ROUTER_NAME, vm.getInstanceName()); + String privateIpAddress = null; + for (final NicProfile profile : getNicProfiles(vm)) { + if (profile == null) { + continue; + } + final Network network = _networksDao.findById(profile.getNetworkId()); + if (network == null) { + continue; + } + final String address = profile.getIPv4Address(); + if (network.getTrafficType() == Networks.TrafficType.Control) { + accessDetails.put(NetworkElementCommand.ROUTER_IP, address); + } + if (network.getTrafficType() == Networks.TrafficType.Guest) { + accessDetails.put(NetworkElementCommand.ROUTER_GUEST_IP, address); + } + if (network.getTrafficType() == Networks.TrafficType.Management) { + privateIpAddress = address; + } + if (network.getTrafficType() != null && !Strings.isNullOrEmpty(address)) { + accessDetails.put(network.getTrafficType().name(), address); + } + } + if (privateIpAddress != null && Strings.isNullOrEmpty(accessDetails.get(NetworkElementCommand.ROUTER_IP))) { + accessDetails.put(NetworkElementCommand.ROUTER_IP, privateIpAddress); + } + return accessDetails; + } + protected boolean stateTransitTo(final NetworkVO network, final Network.Event e) throws NoTransitionException { return _stateMachine.transitTo(network, e, null, _networksDao); } diff --git a/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 8b7a604979e1..e8d6633d1630 100644 --- a/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -130,6 +130,7 @@ + diff --git a/engine/schema/src/com/cloud/certificate/CrlVO.java b/engine/schema/src/com/cloud/certificate/CrlVO.java new file mode 100644 index 000000000000..6df7530b2903 --- /dev/null +++ b/engine/schema/src/com/cloud/certificate/CrlVO.java @@ -0,0 +1,85 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package com.cloud.certificate; + +import java.math.BigInteger; +import java.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.cloudstack.api.InternalIdentity; + +@Entity +@Table(name = "crl") +public class CrlVO implements InternalIdentity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id = null; + + @Column(name = "serial") + private String certSerial; + + @Column(name = "cn") + private String certCn; + + @Column(name = "revoker_uuid") + private String revokerUuid; + + @Temporal(value = TemporalType.TIMESTAMP) + @Column(name = "revoked", updatable = true) + private Date revoked; + + public CrlVO() { + } + + public CrlVO(final BigInteger certSerial, final String certCn, final String revokerUuid) { + this.certSerial = certSerial.toString(16); + this.certCn = certCn; + this.revokerUuid = revokerUuid; + this.revoked = new Date(); + } + + @Override + public long getId() { + return id; + } + + public BigInteger getCertSerial() { + return new BigInteger(certSerial, 16); + } + + public String getCertCn() { + return certCn; + } + + public String getRevokerUuid() { + return revokerUuid; + } + + public Date getRevoked() { + return revoked; + } +} diff --git a/engine/schema/src/com/cloud/certificate/dao/CrlDao.java b/engine/schema/src/com/cloud/certificate/dao/CrlDao.java new file mode 100644 index 000000000000..613d9caf487f --- /dev/null +++ b/engine/schema/src/com/cloud/certificate/dao/CrlDao.java @@ -0,0 +1,28 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package com.cloud.certificate.dao; + +import java.math.BigInteger; + +import com.cloud.certificate.CrlVO; +import com.cloud.utils.db.GenericDao; + +public interface CrlDao extends GenericDao { + CrlVO findBySerial(final BigInteger certSerial); + CrlVO revokeCertificate(final BigInteger certSerial, final String certCn); +} \ No newline at end of file diff --git a/engine/schema/src/com/cloud/certificate/dao/CrlDaoImpl.java b/engine/schema/src/com/cloud/certificate/dao/CrlDaoImpl.java new file mode 100644 index 000000000000..2eee308f1154 --- /dev/null +++ b/engine/schema/src/com/cloud/certificate/dao/CrlDaoImpl.java @@ -0,0 +1,57 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package com.cloud.certificate.dao; + +import java.math.BigInteger; + +import org.apache.cloudstack.context.CallContext; + +import com.cloud.certificate.CrlVO; +import com.cloud.utils.db.DB; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; + +@DB +public class CrlDaoImpl extends GenericDaoBase implements CrlDao { + + private final SearchBuilder CrlBySerialSearch; + + public CrlDaoImpl() { + super(); + + CrlBySerialSearch = createSearchBuilder(); + CrlBySerialSearch.and("certSerial", CrlBySerialSearch.entity().getCertSerial(), SearchCriteria.Op.EQ); + CrlBySerialSearch.done(); + } + + @Override + public CrlVO findBySerial(final BigInteger certSerial) { + if (certSerial == null) { + return null; + } + final SearchCriteria sc = CrlBySerialSearch.create("certSerial", certSerial.toString(16)); + return findOneBy(sc); + } + + @Override + public CrlVO revokeCertificate(final BigInteger certSerial, final String certCn) { + final CrlVO revokedCertificate = new CrlVO(certSerial, certCn == null ? "" : certCn, CallContext.current().getCallingUserUuid()); + return persist(revokedCertificate); + } +} diff --git a/engine/schema/src/com/cloud/host/dao/HostDao.java b/engine/schema/src/com/cloud/host/dao/HostDao.java index 7ffe1ed2ed5a..f98e8c19eebd 100644 --- a/engine/schema/src/com/cloud/host/dao/HostDao.java +++ b/engine/schema/src/com/cloud/host/dao/HostDao.java @@ -101,4 +101,6 @@ public interface HostDao extends GenericDao, StateDao listClustersByHostTag(String hostTagOnOffering); List listByType(Type type); + + HostVO findByIp(String ip); } diff --git a/engine/schema/src/com/cloud/host/dao/HostDaoImpl.java b/engine/schema/src/com/cloud/host/dao/HostDaoImpl.java index 309d17eaa645..0ac4f43b7fd5 100644 --- a/engine/schema/src/com/cloud/host/dao/HostDaoImpl.java +++ b/engine/schema/src/com/cloud/host/dao/HostDaoImpl.java @@ -89,6 +89,7 @@ public class HostDaoImpl extends GenericDaoBase implements HostDao protected SearchBuilder DcPrivateIpAddressSearch; protected SearchBuilder DcStorageIpAddressSearch; protected SearchBuilder PublicIpAddressSearch; + protected SearchBuilder AnyIpAddressSearch; protected SearchBuilder GuidSearch; protected SearchBuilder DcSearch; @@ -216,6 +217,11 @@ public void init() { PublicIpAddressSearch.and("publicIpAddress", PublicIpAddressSearch.entity().getPublicIpAddress(), SearchCriteria.Op.EQ); PublicIpAddressSearch.done(); + AnyIpAddressSearch = createSearchBuilder(); + AnyIpAddressSearch.or("publicIpAddress", AnyIpAddressSearch.entity().getPublicIpAddress(), SearchCriteria.Op.EQ); + AnyIpAddressSearch.or("privateIpAddress", AnyIpAddressSearch.entity().getPrivateIpAddress(), SearchCriteria.Op.EQ); + AnyIpAddressSearch.done(); + GuidSearch = createSearchBuilder(); GuidSearch.and("guid", GuidSearch.entity().getGuid(), SearchCriteria.Op.EQ); GuidSearch.done(); @@ -1118,6 +1124,13 @@ public HostVO findByPublicIp(String publicIp) { return findOneBy(sc); } + @Override + public HostVO findByIp(final String ipAddress) { + SearchCriteria sc = AnyIpAddressSearch.create(); + sc.setParameters("publicIpAddress", ipAddress); + sc.setParameters("privateIpAddress", ipAddress); + return findOneBy(sc); + } @Override public List findHypervisorHostInCluster(long clusterId) { diff --git a/framework/ca/pom.xml b/framework/ca/pom.xml new file mode 100644 index 000000000000..9412649b3b97 --- /dev/null +++ b/framework/ca/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + cloud-framework-ca + Apache CloudStack Framework - Certificate Authority + + org.apache.cloudstack + cloudstack-framework + 4.11.0.0-SNAPSHOT + ../pom.xml + + diff --git a/framework/ca/src/org/apache/cloudstack/framework/ca/CAProvider.java b/framework/ca/src/org/apache/cloudstack/framework/ca/CAProvider.java new file mode 100644 index 000000000000..8dc343bd5927 --- /dev/null +++ b/framework/ca/src/org/apache/cloudstack/framework/ca/CAProvider.java @@ -0,0 +1,93 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.framework.ca; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +public interface CAProvider { + + /** + * Method returns capability of the plugin to participate in certificate issuance, revocation and provisioning + * @return returns true when CA provider can do certificate lifecycle tasks + */ + boolean canProvisionCertificates(); + + /** + * Returns root CA certificate + * @return returns concatenated root CA certificate string + */ + List getCaCertificate(); + + /** + * Issues certificate with provided options + * @param domainNames + * @param ipAddresses + * @param validityDays + * @return returns issued certificate + */ + Certificate issueCertificate(final List domainNames, final List ipAddresses, final int validityDays); + + /** + * Issues certificate using given CSR and other options + * @param csr + * @param domainNames + * @param ipAddresses + * @param validityDays + * @return returns issued certificate using provided CSR and other options + */ + Certificate issueCertificate(final String csr, final List domainNames, final List ipAddresses, final int validityDays); + + /** + * Revokes certificate using certificate serial and CN + * @param certSerial + * @param certCn + * @return returns true on success + */ + boolean revokeCertificate(final BigInteger certSerial, final String certCn); + + /** + * This method can add/inject custom TrustManagers for client connection validations. + * @param sslContext The SSL context used while accepting a client connection + * @param remoteAddress + * @param certMap + * @return returns created SSL engine instance + * @throws GeneralSecurityException + * @throws IOException + */ + SSLEngine createSSLEngine(final SSLContext sslContext, final String remoteAddress, final Map certMap) throws GeneralSecurityException, IOException; + + /** + * Returns the unique name of the provider + * @return returns provider name + */ + String getProviderName(); + + /** + * Returns description about the CA provider plugin + * @return returns description + */ + String getDescription(); +} diff --git a/framework/ca/src/org/apache/cloudstack/framework/ca/CAService.java b/framework/ca/src/org/apache/cloudstack/framework/ca/CAService.java new file mode 100644 index 000000000000..3aacb3b2b858 --- /dev/null +++ b/framework/ca/src/org/apache/cloudstack/framework/ca/CAService.java @@ -0,0 +1,36 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.framework.ca; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +public interface CAService { + /** + * Returns a SSLEngine to be used for handling client connections + * @param context + * @param remoteAddress + * @return + * @throws GeneralSecurityException + * @throws IOException + */ + SSLEngine createSSLEngine(final SSLContext context, final String remoteAddress) throws GeneralSecurityException, IOException; +} diff --git a/framework/ca/src/org/apache/cloudstack/framework/ca/Certificate.java b/framework/ca/src/org/apache/cloudstack/framework/ca/Certificate.java new file mode 100644 index 000000000000..b3a230d5a992 --- /dev/null +++ b/framework/ca/src/org/apache/cloudstack/framework/ca/Certificate.java @@ -0,0 +1,46 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.framework.ca; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.List; + +public class Certificate { + private X509Certificate clientCertificate; + private PrivateKey privateKey; + private List caCertificates; + + public Certificate(final X509Certificate clientCertificate, final PrivateKey privateKey, final List caCertificates) { + this.clientCertificate = clientCertificate; + this.privateKey = privateKey; + this.caCertificates = caCertificates; + } + + public X509Certificate getClientCertificate() { + return clientCertificate; + } + + public PrivateKey getPrivateKey() { + return privateKey; + } + + public List getCaCertificates() { + return caCertificates; + } +} diff --git a/framework/pom.xml b/framework/pom.xml index 3fccc4c383d0..5bfb1d094fb3 100644 --- a/framework/pom.xml +++ b/framework/pom.xml @@ -44,6 +44,7 @@ ipc + ca rest events jobs diff --git a/packaging/centos63/cloud.spec b/packaging/centos63/cloud.spec index abe50bce65e2..dbf55bb580bf 100644 --- a/packaging/centos63/cloud.spec +++ b/packaging/centos63/cloud.spec @@ -499,12 +499,6 @@ else echo "Unable to determine ssl settings for tomcat.conf, please run cloudstack-setup-management manually" fi -if [ -f "%{_sysconfdir}/cloud.rpmsave/management/cloud.keystore" ]; then - cp -p %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore %{_sysconfdir}/%{name}/management/cloudmanagementserver.keystore - # make sure we only do this on the first install of this RPM, don't want to overwrite on a reinstall - mv %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore.rpmsave -fi - %preun agent /sbin/service cloudstack-agent stop || true if [ "$1" == "0" ] ; then diff --git a/packaging/fedora20/cloud.spec b/packaging/fedora20/cloud.spec index 546e439da2b7..3f960cf74918 100644 --- a/packaging/fedora20/cloud.spec +++ b/packaging/fedora20/cloud.spec @@ -464,12 +464,6 @@ else echo "Unable to determine ssl settings for tomcat.conf, please run cloudstack-setup-management manually" fi -if [ -f "%{_sysconfdir}/cloud.rpmsave/management/cloud.keystore" ]; then - cp -p %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore %{_sysconfdir}/%{name}/management/cloudmanagementserver.keystore - # make sure we only do this on the first install of this RPM, don't want to overwrite on a reinstall - mv %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore.rpmsave -fi - %preun agent /sbin/service cloudstack-agent stop || true if [ "$1" == "0" ] ; then diff --git a/packaging/fedora21/cloud.spec b/packaging/fedora21/cloud.spec index a79d172eab79..de05370c86b7 100644 --- a/packaging/fedora21/cloud.spec +++ b/packaging/fedora21/cloud.spec @@ -464,12 +464,6 @@ else echo "Unable to determine ssl settings for tomcat.conf, please run cloudstack-setup-management manually" fi -if [ -f "%{_sysconfdir}/cloud.rpmsave/management/cloud.keystore" ]; then - cp -p %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore %{_sysconfdir}/%{name}/management/cloudmanagementserver.keystore - # make sure we only do this on the first install of this RPM, don't want to overwrite on a reinstall - mv %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore %{_sysconfdir}/cloud.rpmsave/management/cloud.keystore.rpmsave -fi - %preun agent /sbin/service cloudstack-agent stop || true if [ "$1" == "0" ] ; then diff --git a/packaging/systemd/cloudstack-management.default b/packaging/systemd/cloudstack-management.default index 6e5fcf9d8d69..eb5b654b1b04 100644 --- a/packaging/systemd/cloudstack-management.default +++ b/packaging/systemd/cloudstack-management.default @@ -28,8 +28,8 @@ JASPER_HOME="/usr/share/cloudstack-management" CATALINA_TMPDIR="/usr/share/cloudstack-management/temp" -if [ -r "/etc/cloudstack/management/cloudmanagementserver.keystore" ] ; then - JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:PermSize=512M -XX:MaxPermSize=800m -Djavax.net.ssl.trustStore=/etc/cloudstack/management/cloudmanagementserver.keystore -Djavax.net.ssl.trustStorePassword=vmops.com " +if [ -r "/etc/cloudstack/management/cloud.jks" ] ; then + JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:PermSize=512M -XX:MaxPermSize=800m -Djavax.net.ssl.trustStore=/etc/cloudstack/management/cloud.jks -Djavax.net.ssl.trustStorePassword=vmops.com " else JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:PermSize=512M -XX:MaxPermSize=800m" fi diff --git a/packaging/systemd/cloudstack-management.default.ubuntu b/packaging/systemd/cloudstack-management.default.ubuntu index 0087495dbcd8..9b3d0c475b5a 100644 --- a/packaging/systemd/cloudstack-management.default.ubuntu +++ b/packaging/systemd/cloudstack-management.default.ubuntu @@ -28,8 +28,8 @@ JASPER_HOME="/usr/share/cloudstack-management" CATALINA_TMPDIR="/usr/share/cloudstack-management/temp" -if [ -r "/etc/cloudstack/management/cloudmanagementserver.keystore" ] ; then - JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:PermSize=512M -XX:MaxPermSize=800m -Djavax.net.ssl.trustStore=/etc/cloudstack/management/cloudmanagementserver.keystore -Djavax.net.ssl.trustStorePassword=vmops.com " +if [ -r "/etc/cloudstack/management/cloud.jks" ] ; then + JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:PermSize=512M -XX:MaxPermSize=800m -Djavax.net.ssl.trustStore=/etc/cloudstack/management/cloud.jks -Djavax.net.ssl.trustStorePassword=vmops.com " else JAVA_OPTS="-Djava.awt.headless=true -Dcom.sun.management.jmxremote=false -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cloudstack/management/ -XX:PermSize=512M -XX:MaxPermSize=800m" fi diff --git a/plugins/ca/root-ca/pom.xml b/plugins/ca/root-ca/pom.xml new file mode 100644 index 000000000000..e27f49112239 --- /dev/null +++ b/plugins/ca/root-ca/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + cloud-plugin-ca-rootca + Apache CloudStack Plugin - Inbuilt Root Certificate Authority + + org.apache.cloudstack + cloudstack-plugins + 4.11.0.0-SNAPSHOT + ../../pom.xml + + + + org.apache.cloudstack + cloud-utils + ${project.version} + + + org.apache.cloudstack + cloud-api + ${project.version} + + + org.apache.cloudstack + cloud-framework-ca + ${project.version} + + + diff --git a/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/module.properties b/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/module.properties new file mode 100644 index 000000000000..e086bc03073b --- /dev/null +++ b/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/module.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +name=root-ca +parent=ca diff --git a/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/spring-root-ca-context.xml b/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/spring-root-ca-context.xml new file mode 100644 index 000000000000..46503febd7b6 --- /dev/null +++ b/plugins/ca/root-ca/resources/META-INF/cloudstack/root-ca/spring-root-ca-context.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java b/plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java new file mode 100644 index 000000000000..90f620393ee2 --- /dev/null +++ b/plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCACustomTrustManager.java @@ -0,0 +1,146 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.ca.provider; + +import java.math.BigInteger; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.X509TrustManager; + +import org.apache.log4j.Logger; + +import com.cloud.certificate.dao.CrlDao; +import com.google.common.base.Strings; + +public final class RootCACustomTrustManager implements X509TrustManager { + private static final Logger LOG = Logger.getLogger(RootCACustomTrustManager.class); + + private String clientAddress = "Unknown"; + private boolean authStrictness = true; + private boolean allowExpiredCertificate = true; + private CrlDao crlDao; + private X509Certificate caCertificate; + private Map activeCertMap; + + public RootCACustomTrustManager(final String clientAddress, final boolean authStrictness, final boolean allowExpiredCertificate, final Map activeCertMap, final X509Certificate caCertificate, final CrlDao crlDao) { + if (!Strings.isNullOrEmpty(clientAddress)) { + this.clientAddress = clientAddress.replace("/", "").split(":")[0]; + } + this.authStrictness = authStrictness; + this.allowExpiredCertificate = allowExpiredCertificate; + this.activeCertMap = activeCertMap; + this.caCertificate = caCertificate; + this.crlDao = crlDao; + } + + private void printCertificateChain(final X509Certificate[] certificates, final String s) throws CertificateException { + if (certificates == null) { + return; + } + final StringBuilder builder = new StringBuilder(); + builder.append("A client/agent attempting connection from address=").append(clientAddress).append(" has presented these certificate(s):"); + int counter = 1; + for (final X509Certificate certificate: certificates) { + builder.append("\nCertificate [").append(counter++).append("] :"); + builder.append(String.format("\n Serial: %x", certificate.getSerialNumber())); + builder.append("\n Not Before:" + certificate.getNotBefore()); + builder.append("\n Not After:" + certificate.getNotAfter()); + builder.append("\n Signature Algorithm:" + certificate.getSigAlgName()); + builder.append("\n Version:" + certificate.getVersion()); + builder.append("\n Subject DN:" + certificate.getSubjectDN()); + builder.append("\n Issuer DN:" + certificate.getIssuerDN()); + builder.append("\n Alternative Names:" + certificate.getSubjectAlternativeNames()); + } + LOG.debug(builder.toString()); + } + + @Override + public void checkClientTrusted(final X509Certificate[] certificates, final String s) throws CertificateException { + if (LOG.isDebugEnabled()) { + printCertificateChain(certificates, s); + } + if (!authStrictness) { + return; + } + if (certificates == null || certificates.length < 1 || certificates[0] == null) { + throw new CertificateException("In strict auth mode, certificate(s) are expected from client:" + clientAddress); + } + final X509Certificate primaryClientCertificate = certificates[0]; + + // Revocation check + final BigInteger serialNumber = primaryClientCertificate.getSerialNumber(); + if (serialNumber == null || crlDao.findBySerial(serialNumber) != null) { + final String errorMsg = String.format("Client is using revoked certificate of serial=%x, subject=%s from address=%s", + primaryClientCertificate.getSerialNumber(), primaryClientCertificate.getSubjectDN(), clientAddress); + LOG.error(errorMsg); + throw new CertificateException(errorMsg); + } + + // Validity check + if (!allowExpiredCertificate) { + try { + primaryClientCertificate.checkValidity(); + } catch (final CertificateExpiredException | CertificateNotYetValidException e) { + final String errorMsg = String.format("Client certificate has expired with serial=%x, subject=%s from address=%s", + primaryClientCertificate.getSerialNumber(), primaryClientCertificate.getSubjectDN(), clientAddress); + LOG.error(errorMsg); + throw new CertificateException(errorMsg); } + } + + // Ownership check + boolean certMatchesOwnership = false; + if (primaryClientCertificate.getSubjectAlternativeNames() != null) { + for (final List list : primaryClientCertificate.getSubjectAlternativeNames()) { + if (list != null && list.size() == 2 && list.get(1) instanceof String) { + final String alternativeName = (String) list.get(1); + if (clientAddress.equals(alternativeName)) { + certMatchesOwnership = true; + } + } + } + } + if (!certMatchesOwnership) { + final String errorMsg = "Certificate ownership verification failed for client: " + clientAddress; + LOG.error(errorMsg); + throw new CertificateException(errorMsg); + } + if (activeCertMap != null && !Strings.isNullOrEmpty(clientAddress)) { + activeCertMap.put(clientAddress, primaryClientCertificate); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Client/agent connection from ip=" + clientAddress + " has been validated and trusted."); + } + } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + if (!authStrictness) { + return null; + } + return new X509Certificate[]{caCertificate}; + } +} diff --git a/plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCAProvider.java b/plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCAProvider.java new file mode 100644 index 000000000000..1f39853568b4 --- /dev/null +++ b/plugins/ca/root-ca/src/org/apache/cloudstack/ca/provider/RootCAProvider.java @@ -0,0 +1,465 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.ca.provider; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.StringReader; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyManagementException; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.Security; +import java.security.SignatureException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.apache.cloudstack.ca.CAManager; +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.dao.ConfigurationDao; +import org.apache.cloudstack.utils.security.CertUtils; +import org.apache.cloudstack.utils.security.KeyStoreUtils; +import org.apache.log4j.Logger; +import org.bouncycastle.jce.PKCS10CertificationRequest; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; + +import com.cloud.certificate.dao.CrlDao; +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.component.AdapterBase; +import com.cloud.utils.db.DbProperties; +import com.cloud.utils.db.GlobalLock; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.NetUtils; +import com.cloud.utils.nio.Link; +import com.google.common.base.Strings; + +public final class RootCAProvider extends AdapterBase implements CAProvider, Configurable { + private static final Logger LOG = Logger.getLogger(RootCAProvider.class); + + public static final Integer caValidityYears = 30; + public static final String caAlias = "root"; + public static final String managementAlias = "management"; + + private static KeyPair caKeyPair = null; + private static X509Certificate caCertificate = null; + + @Inject + private ConfigurationDao configDao; + @Inject + private CrlDao crlDao; + + //////////////////////////////////////////////////// + /////////////// Root CA Settings /////////////////// + //////////////////////////////////////////////////// + + private static ConfigKey rootCAPrivateKey = new ConfigKey<>("Hidden", String.class, + "ca.plugin.root.private.key", + null, + "The ROOT CA private key.", true); + + private static ConfigKey rootCAPublicKey = new ConfigKey<>("Hidden", String.class, + "ca.plugin.root.public.key", + null, + "The ROOT CA public key.", true); + + private static ConfigKey rootCACertificate = new ConfigKey<>("Hidden", String.class, + "ca.plugin.root.ca.certificate", + null, + "The ROOT CA certificate.", true); + + private static ConfigKey rootCAIssuerDN = new ConfigKey<>("Advanced", String.class, + "ca.plugin.root.issuer.dn", + "CN=ca.cloudstack.apache.org", + "The ROOT CA issuer distinguished name.", true); + + protected static ConfigKey rootCAAuthStrictness = new ConfigKey<>("Advanced", Boolean.class, + "ca.plugin.root.auth.strictness", + "false", + "Set client authentication strictness, setting to true will enforce and require client certificate for authentication in applicable CA providers.", true); + + private static ConfigKey rootCAAllowExpiredCert = new ConfigKey<>("Advanced", Boolean.class, + "ca.plugin.root.allow.expired.cert", + "true", + "When set to true, it will allow expired client certificate during SSL handshake.", true); + + + /////////////////////////////////////////////////////////// + /////////////// Root CA Private Methods /////////////////// + /////////////////////////////////////////////////////////// + + private Certificate generateCertificate(final List domainNames, final List ipAddresses, final int validityDays) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, CertificateException, SignatureException, IOException, OperatorCreationException { + if (domainNames == null || domainNames.size() < 1 || Strings.isNullOrEmpty(domainNames.get(0))) { + throw new CloudRuntimeException("No domain name is specified, cannot generate certificate"); + } + final String subject = "CN=" + domainNames.get(0); + + final KeyPair keyPair = CertUtils.generateRandomKeyPair(CAManager.CertKeySize.value()); + final X509Certificate clientCertificate = CertUtils.generateV3Certificate( + caCertificate, + caKeyPair.getPrivate(), + keyPair.getPublic(), + subject, + CAManager.CertSignatureAlgorithm.value(), + validityDays, + domainNames, + ipAddresses); + return new Certificate(clientCertificate, keyPair.getPrivate(), Collections.singletonList(caCertificate)); + } + + private Certificate generateCertificateUsingCsr(final String csr, final List domainNames, final List ipAddresses, final int validityDays) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, CertificateException, SignatureException, IOException, OperatorCreationException { + PemObject pemObject = null; + + try { + final PemReader pemReader = new PemReader(new StringReader(csr)); + pemObject = pemReader.readPemObject(); + } catch (IOException e) { + LOG.error("Failed to read provided CSR string as a PEM object", e); + } + + if (pemObject == null) { + throw new CloudRuntimeException("Unable to read/process CSR: " + csr); + } + + final PKCS10CertificationRequest request = new PKCS10CertificationRequest(pemObject.getContent()); + + final X509Certificate clientCertificate = CertUtils.generateV3Certificate( + caCertificate, caKeyPair.getPrivate(), + request.getPublicKey(), + request.getCertificationRequestInfo().getSubject().toString(), + CAManager.CertSignatureAlgorithm.value(), + validityDays, + domainNames, + ipAddresses); + return new Certificate(clientCertificate, null, Collections.singletonList(caCertificate)); + + } + + //////////////////////////////////////////////////////// + /////////////// Root CA API Handlers /////////////////// + //////////////////////////////////////////////////////// + + @Override + public boolean canProvisionCertificates() { + return true; + } + + @Override + public List getCaCertificate() { + return Collections.singletonList(caCertificate); + } + + @Override + public Certificate issueCertificate(final List domainNames, final List ipAddresses, final int validityDays) { + try { + return generateCertificate(domainNames, ipAddresses, validityDays); + } catch (final CertificateException | IOException | SignatureException | NoSuchAlgorithmException | NoSuchProviderException | InvalidKeyException | OperatorCreationException e) { + LOG.error("Failed to create client certificate, due to: ", e); + throw new CloudRuntimeException("Failed to generate certificate due to:" + e.getMessage()); + } + } + + @Override + public Certificate issueCertificate(final String csr, final List domainNames, final List ipAddresses, final int validityDays) { + try { + return generateCertificateUsingCsr(csr, domainNames, ipAddresses, validityDays); + } catch (final CertificateException | IOException | SignatureException | NoSuchAlgorithmException | NoSuchProviderException | InvalidKeyException | OperatorCreationException e) { + LOG.error("Failed to generate certificate from CSR: ", e); + throw new CloudRuntimeException("Failed to generate certificate using CSR due to:" + e.getMessage()); + } + } + + @Override + public boolean revokeCertificate(final BigInteger certSerial, final String certCn) { + return true; + } + + //////////////////////////////////////////////////////////// + /////////////// Root CA Trust Management /////////////////// + //////////////////////////////////////////////////////////// + + private char[] getCaKeyStorePassphrase() { + return KeyStoreUtils.defaultKeystorePassphrase; + } + + 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(), getCaKeyStorePassphrase(), new X509Certificate[]{caCertificate}); + } else { + return null; + } + return ks; + } + + @Override + public SSLEngine createSSLEngine(final SSLContext sslContext, final String remoteAddress, final Map certMap) throws KeyManagementException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException { + final KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + final TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + + final KeyStore ks = getCaKeyStore(); + kmf.init(ks, getCaKeyStorePassphrase()); + tmf.init(ks); + + final boolean authStrictness = rootCAAuthStrictness.value(); + final boolean allowExpiredCertificate = rootCAAllowExpiredCert.value(); + + TrustManager[] tms = new TrustManager[]{new RootCACustomTrustManager(remoteAddress, authStrictness, allowExpiredCertificate, certMap, caCertificate, crlDao)}; + sslContext.init(kmf.getKeyManagers(), tms, new SecureRandom()); + final SSLEngine sslEngine = sslContext.createSSLEngine(); + sslEngine.setWantClientAuth(authStrictness); + return sslEngine; + } + + ////////////////////////////////////////////////// + /////////////// Root CA Config /////////////////// + ////////////////////////////////////////////////// + + private char[] findKeyStorePassphrase() { + char[] passphrase = KeyStoreUtils.defaultKeystorePassphrase; + final String configuredPassphrase = DbProperties.getDbProperties().getProperty("db.cloud.keyStorePassphrase"); + if (configuredPassphrase != null) { + passphrase = configuredPassphrase.toCharArray(); + } + return passphrase; + } + + private boolean createManagementServerKeystore(final String keyStoreFilePath, final char[] passphrase) { + final Certificate managementServerCertificate = issueCertificate(Collections.singletonList(NetUtils.getHostName()), + Collections.singletonList(NetUtils.getDefaultHostIp()), caValidityYears * 365); + if (managementServerCertificate == null || managementServerCertificate.getPrivateKey() == null) { + throw new CloudRuntimeException("Failed to generate certificate and setup management server keystore"); + } + LOG.info("Creating new management server certificate and keystore"); + try { + final KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, null); + keyStore.setCertificateEntry(caAlias, caCertificate); + keyStore.setKeyEntry(managementAlias, managementServerCertificate.getPrivateKey(), passphrase, + new X509Certificate[]{managementServerCertificate.getClientCertificate(), caCertificate}); + final String tmpFile = KeyStoreUtils.defaultTmpKeyStoreFile; + final FileOutputStream stream = new FileOutputStream(tmpFile); + keyStore.store(stream, passphrase); + stream.close(); + KeyStoreUtils.copyKeystore(keyStoreFilePath, tmpFile); + LOG.debug("Saved default root CA (server) keystore file at:" + keyStoreFilePath); + } catch (final CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException e) { + LOG.error("Failed to save root CA (server) keystore due to exception: ", e); + return false; + } + return true; + } + + private boolean checkManagementServerKeystore() { + final File confFile = PropertiesUtil.findConfigFile("db.properties"); + if (confFile == null) { + return false; + } + final char[] passphrase = findKeyStorePassphrase(); + final String keystorePath = confFile.getParent() + "/" + KeyStoreUtils.defaultKeystoreFile; + final File keystoreFile = new File(keystorePath); + if (keystoreFile.exists()) { + try { + final KeyStore msKeystore = Link.loadKeyStore(new FileInputStream(keystorePath), passphrase); + try { + final java.security.cert.Certificate[] msCertificates = msKeystore.getCertificateChain(managementAlias); + if (msCertificates != null && msCertificates.length > 1) { + msCertificates[0].verify(caKeyPair.getPublic()); + ((X509Certificate)msCertificates[0]).checkValidity(); + return true; + } + } catch (final CertificateException | NoSuchAlgorithmException | InvalidKeyException | NoSuchProviderException | SignatureException e) { + LOG.info("Renewing management server keystore, current certificate has expired"); + return createManagementServerKeystore(keystoreFile.getAbsolutePath(), passphrase); + } + } catch (final GeneralSecurityException | IOException e) { + LOG.error("Failed to read current management server keystore, renewing keystore!"); + } + } + return createManagementServerKeystore(keystoreFile.getAbsolutePath(), passphrase); + } + + ///////////////////////////////////////////////// + /////////////// Root CA Setup /////////////////// + ///////////////////////////////////////////////// + + private boolean saveNewRootCAKeypair() { + try { + LOG.debug("Generating root CA public/private keys"); + final KeyPair keyPair = CertUtils.generateRandomKeyPair(2 * CAManager.CertKeySize.value()); + if (!configDao.update(rootCAPublicKey.key(), rootCAPublicKey.category(), CertUtils.publicKeyToPem(keyPair.getPublic()))) { + LOG.error("Failed to save RootCA public key"); + } + if (!configDao.update(rootCAPrivateKey.key(), rootCAPrivateKey.category(), CertUtils.privateKeyToPem(keyPair.getPrivate()))) { + LOG.error("Failed to save RootCA private key"); + } + } catch (final NoSuchProviderException | NoSuchAlgorithmException | IOException e) { + LOG.error("Failed to generate/save RootCA private/public keys due to exception:", e); + } + return loadRootCAKeyPair(); + } + + private boolean saveNewRootCACertificate() { + if (caKeyPair == null) { + throw new CloudRuntimeException("Cannot issue self-signed root CA certificate as CA keypair is not initialized"); + } + try { + LOG.debug("Generating root CA certificate"); + final X509Certificate rootCaCertificate = CertUtils.generateV1Certificate( + caKeyPair, + rootCAIssuerDN.value(), + rootCAIssuerDN.value(), + caValidityYears, + CAManager.CertSignatureAlgorithm.value()); + if (!configDao.update(rootCACertificate.key(), rootCACertificate.category(), CertUtils.x509CertificateToPem(rootCaCertificate))) { + LOG.error("Failed to update RootCA public/x509 certificate"); + } + } catch (final CertificateException | NoSuchAlgorithmException | NoSuchProviderException | SignatureException | InvalidKeyException | OperatorCreationException | IOException e) { + LOG.error("Failed to generate RootCA certificate from private/public keys due to exception:", e); + return false; + } + return loadRootCACertificate(); + } + + private boolean loadRootCAKeyPair() { + if (Strings.isNullOrEmpty(rootCAPublicKey.value()) || Strings.isNullOrEmpty(rootCAPrivateKey.value())) { + return false; + } + try { + caKeyPair = new KeyPair(CertUtils.pemToPublicKey(rootCAPublicKey.value()), CertUtils.pemToPrivateKey(rootCAPrivateKey.value())); + } catch (InvalidKeySpecException | IOException e) { + LOG.error("Failed to load saved RootCA private/public keys due to exception:", e); + return false; + } + return caKeyPair.getPrivate() != null && caKeyPair.getPublic() != null; + } + + private boolean loadRootCACertificate() { + if (Strings.isNullOrEmpty(rootCACertificate.value())) { + return false; + } + try { + caCertificate = CertUtils.pemToX509Certificate(rootCACertificate.value()); + caCertificate.verify(caKeyPair.getPublic()); + } catch (final IOException | CertificateException | NoSuchAlgorithmException | InvalidKeyException | SignatureException | NoSuchProviderException e) { + LOG.error("Failed to load saved RootCA certificate due to exception:", e); + return false; + } + return caCertificate != null; + } + + private boolean setupCA() { + if (!loadRootCAKeyPair() && !saveNewRootCAKeypair()) { + LOG.error("Failed to save and load root CA keypair"); + return false; + } + if (!loadRootCACertificate() && !saveNewRootCACertificate()) { + LOG.error("Failed to save and load root CA certificate"); + return false; + } + if (!checkManagementServerKeystore()) { + LOG.error("Failed to check and configure management server keystore"); + return false; + } + return true; + } + + @Override + public boolean start() { + return loadRootCAKeyPair() && loadRootCAKeyPair() && checkManagementServerKeystore(); + } + + @Override + public boolean configure(String name, Map params) throws ConfigurationException { + super.configure(name, params); + Security.addProvider(new BouncyCastleProvider()); + final GlobalLock caLock = GlobalLock.getInternLock("RootCAProviderSetup"); + try { + if (caLock.lock(5 * 60)) { + try { + return setupCA(); + } finally { + caLock.unlock(); + } + } else { + LOG.error("Failed to grab lock and setup CA, startup method will try to load the CA certificate and keypair."); + } + } finally { + caLock.releaseRef(); + } + return true; + } + + /////////////////////////////////////////////////////// + /////////////// Root CA Descriptors /////////////////// + /////////////////////////////////////////////////////// + + @Override + public String getConfigComponentName() { + return RootCAProvider.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ + rootCAPrivateKey, + rootCAPublicKey, + rootCACertificate, + rootCAIssuerDN, + rootCAAuthStrictness, + rootCAAllowExpiredCert + }; + } + + @Override + public String getProviderName() { + return "root"; + } + + @Override + public String getDescription() { + return "CloudStack's Root CA provider plugin"; + } +} diff --git a/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java b/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java new file mode 100644 index 000000000000..cab1941fa966 --- /dev/null +++ b/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCACustomTrustManagerTest.java @@ -0,0 +1,111 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca.provider; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.cloudstack.utils.security.CertUtils; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import com.cloud.certificate.CrlVO; +import com.cloud.certificate.dao.CrlDao; + +@RunWith(MockitoJUnitRunner.class) +public class RootCACustomTrustManagerTest { + + @Mock + private CrlDao crlDao; + private KeyPair caKeypair; + private KeyPair clientKeypair; + private X509Certificate caCertificate; + private X509Certificate expiredClientCertificate; + private String clientIp = "1.2.3.4"; + private Map certMap = new HashMap<>(); + + @Before + public void setUp() throws Exception { + certMap.clear(); + caKeypair = CertUtils.generateRandomKeyPair(1024); + clientKeypair = CertUtils.generateRandomKeyPair(1024); + caCertificate = CertUtils.generateV1Certificate(caKeypair, "CN=ca", "CN=ca", 1, + "SHA256withRSA"); + expiredClientCertificate = CertUtils.generateV3Certificate(caCertificate, caKeypair.getPrivate(), clientKeypair.getPublic(), + "CN=cloudstack.apache.org", "SHA256withRSA", 0, Collections.singletonList("cloudstack.apache.org"), Collections.singletonList(clientIp)); + } + + @Test + public void testAuthNotStrict() throws Exception { + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, false, true, certMap, caCertificate, crlDao); + trustManager.checkClientTrusted(null, null); + Assert.assertNull(trustManager.getAcceptedIssuers()); + } + + @Test(expected = CertificateException.class) + public void testAuthStrictWithInvalidCert() throws Exception { + final RootCACustomTrustManager trustManager = new RootCACustomTrustManager(clientIp, true, true, certMap, 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); + 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); + 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); + trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA"); + } + + @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); + Assert.assertTrue(trustManager.getAcceptedIssuers() != null); + Assert.assertTrue(trustManager.getAcceptedIssuers().length == 1); + Assert.assertEquals(trustManager.getAcceptedIssuers()[0], caCertificate); + trustManager.checkClientTrusted(new X509Certificate[]{expiredClientCertificate}, "RSA"); + Assert.assertTrue(certMap.containsKey(clientIp)); + Assert.assertEquals(certMap.get(clientIp), expiredClientCertificate); + } + +} \ No newline at end of file diff --git a/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCAProviderTest.java b/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCAProviderTest.java new file mode 100644 index 000000000000..bdd3f08ddfe5 --- /dev/null +++ b/plugins/ca/root-ca/test/org/apache/cloudstack/ca/provider/RootCAProviderTest.java @@ -0,0 +1,155 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca.provider; + +import java.lang.reflect.Field; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import javax.net.ssl.SSLEngine; + +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.utils.security.CertUtils; +import org.apache.cloudstack.utils.security.SSLUtils; +import org.joda.time.DateTime; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class RootCAProviderTest { + + private KeyPair caKeyPair; + private X509Certificate caCertificate; + + private RootCAProvider provider; + + private void addField(final RootCAProvider provider, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = RootCAProvider.class.getDeclaredField(name); + f.setAccessible(true); + f.set(provider, o); + } + + private void overrideDefaultConfigValue(final ConfigKey configKey, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = ConfigKey.class.getDeclaredField(name); + f.setAccessible(true); + f.set(configKey, o); + } + + @Before + public void setUp() throws Exception { + caKeyPair = CertUtils.generateRandomKeyPair(1024); + caCertificate = CertUtils.generateV1Certificate(caKeyPair, "CN=ca", "CN=ca", 1, "SHA256withRSA"); + + provider = new RootCAProvider(); + + addField(provider, "caKeyPair", caKeyPair); + addField(provider, "caCertificate", caCertificate); + addField(provider, "caKeyPair", caKeyPair); + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void testCanProvisionCertificates() { + Assert.assertTrue(provider.canProvisionCertificates()); + } + + @Test + public void testGetCaCertificate() { + Assert.assertTrue(provider.getCaCertificate().size() == 1); + Assert.assertEquals(provider.getCaCertificate().get(0), caCertificate); + } + + @Test + public void testIssueCertificateWithoutCsr() throws NoSuchProviderException, CertificateException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { + final Certificate certificate = provider.issueCertificate(Arrays.asList("domain1.com", "domain2.com"), null, 1); + Assert.assertTrue(certificate != null); + Assert.assertTrue(certificate.getPrivateKey() != null); + Assert.assertEquals(certificate.getCaCertificates().get(0), caCertificate); + Assert.assertEquals(certificate.getClientCertificate().getIssuerDN(), caCertificate.getIssuerDN()); + Assert.assertTrue(certificate.getClientCertificate().getNotAfter().before(new DateTime().plusDays(1).toDate())); + certificate.getClientCertificate().verify(caCertificate.getPublicKey()); + } + + @Test + public void testIssueCertificateWithCsr() throws NoSuchProviderException, CertificateException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { + final String csr = "-----BEGIN NEW CERTIFICATE REQUEST-----\n" + + "MIICxTCCAa0CAQAwUDETMBEGA1UEBhMKY2xvdWRzdGFjazETMBEGA1UEChMKY2xvdWRzdGFjazET\n" + + "MBEGA1UECxMKY2xvdWRzdGFjazEPMA0GA1UEAxMGdi0xLVZNMIIBIjANBgkqhkiG9w0BAQEFAAOC\n" + + "AQ8AMIIBCgKCAQEAhi3hOrt/p0hUmoW2A+2gFAMxSINItRrHfQ6VUnHhYKZGcTN9honVFuu30tz7\n" + + "oSLUUx1laWEWLlIozpUcPSjOuPa5a0JS8kjplMd8DLfLNeQ6gcuEWznMRJqCaKM72qn/FAK3r11l\n" + + "2NofEfWbHU5QVQ5CsYF0JndspLcnmf0tnmreAzz6vlSEPQd4g2hTSsPb72eAqYd0eJnl2oXe7cF3\n" + + "iemg6/lWoxlh8njVFDKJ5ibNQA/RSc5syzzaQ8fn/AkZlChR5pml47elfC3GuqetfZPAEP4rebXV\n" + + "zEw+UVbMo5bWx4AYm1S2HxhmsWC/1J5oxluZDtC6tjMqnkKQze8HbQIDAQABoDAwLgYJKoZIhvcN\n" + + "AQkOMSEwHzAdBgNVHQ4EFgQUdgA1C/7vW3lUcb/dnolGjZB55/AwDQYJKoZIhvcNAQELBQADggEB\n" + + "AH6ynWbyW5o4h2yEvmcr+upmu/LZYkpfwIWIo+dfrHX9OHu0rhHDIgMgqEStWzrOfhAkcEocQo21\n" + + "E4Q39nECO+cgTCQ1nfH5BVqaMEg++n6tqXBwLmAQJkftEmB+YUPFB9OGn5TQY9Pcnof95Y8xnvtR\n" + + "0DvVQa9RM9IsqxgvU4wQCcaNHuEC46Wzo7lyYJ6p//GLw8UQnHxsWktt8U+vyaqXjOvz0+nJobUz\n" + + "Jv7r7DFkOwgS6ObBczaZsv1yx2YklcKfbsI7xVsvZAXFey2RsvSJi1QPEJC5XbwDenWnCSrPfjJg\n" + + "SLJ0p9tV70D6v07r1OOmBtvU5AH4N+vioAZA0BE=\n" + + "-----END NEW CERTIFICATE REQUEST-----\n"; + final Certificate certificate = provider.issueCertificate(csr, Arrays.asList("v-1-VM", "domain1.com", "domain2.com"), null, 1); + Assert.assertTrue(certificate != null); + Assert.assertTrue(certificate.getPrivateKey() == null); + Assert.assertEquals(certificate.getCaCertificates().get(0), caCertificate); + Assert.assertTrue(certificate.getClientCertificate().getSubjectDN().toString().startsWith("CN=v-1-VM,")); + certificate.getClientCertificate().verify(caCertificate.getPublicKey()); + } + + @Test + public void testRevokeCertificate() throws Exception { + Assert.assertTrue(provider.revokeCertificate(CertUtils.generateRandomBigInt(), "anyString")); + } + + @Test + public void testCreateSSLEngineWithoutAuthStrictness() throws Exception { + overrideDefaultConfigValue(RootCAProvider.rootCAAuthStrictness, "_defaultValue", "false"); + final SSLEngine e = provider.createSSLEngine(SSLUtils.getSSLContext(), "/1.2.3.4:5678", null); + Assert.assertFalse(e.getUseClientMode()); + Assert.assertFalse(e.getWantClientAuth()); + } + + @Test + public void testCreateSSLEngineWithAuthStrictness() throws Exception { + overrideDefaultConfigValue(RootCAProvider.rootCAAuthStrictness, "_defaultValue", "true"); + final SSLEngine e = provider.createSSLEngine(SSLUtils.getSSLContext(), "/1.2.3.4:5678", null); + Assert.assertFalse(e.getUseClientMode()); + Assert.assertTrue(e.getWantClientAuth()); + } + + @Test + public void testGetProviderName() throws Exception { + Assert.assertEquals(provider.getProviderName(), "root"); + } + +} \ No newline at end of file diff --git a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManager.java b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManager.java index 0851023a7ef0..3a315509b499 100644 --- a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManager.java +++ b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManager.java @@ -20,6 +20,9 @@ import javax.naming.ConfigurationException; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.ca.SetupKeyStoreCommand; + import com.cloud.agent.api.Answer; import com.cloud.agent.api.CheckHealthCommand; import com.cloud.agent.api.CheckNetworkCommand; @@ -52,6 +55,10 @@ boolean handleSystemVMStart(long vmId, String privateIpAddress, String privateMa Answer pingTest(PingTestCommand cmd); + Answer setupKeyStore(SetupKeyStoreCommand cmd); + + Answer setupCertificate(SetupCertificateCommand cmd); + MockHost getHost(String guid); Answer maintain(MaintainCommand cmd); diff --git a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManagerImpl.java b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManagerImpl.java index 9211214ccdc9..9d1e4079d42e 100644 --- a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManagerImpl.java +++ b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/MockAgentManagerImpl.java @@ -31,7 +31,10 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; -import com.cloud.user.AccountManager; +import org.apache.cloudstack.ca.SetupCertificateAnswer; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.ca.SetupKeyStoreCommand; +import org.apache.cloudstack.ca.SetupKeystoreAnswer; import org.apache.cloudstack.context.CallContext; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; @@ -62,6 +65,7 @@ import com.cloud.simulator.MockVMVO; import com.cloud.simulator.dao.MockHostDao; import com.cloud.simulator.dao.MockVMDao; +import com.cloud.user.AccountManager; import com.cloud.utils.Pair; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.concurrency.NamedThreadFactory; @@ -459,6 +463,24 @@ public Answer pingTest(PingTestCommand cmd) { return new Answer(cmd); } + @Override + public Answer setupKeyStore(SetupKeyStoreCommand cmd) { + return new SetupKeystoreAnswer( + "-----BEGIN CERTIFICATE REQUEST-----\n" + + "MIIBHjCByQIBADBkMQswCQYDVQQGEwJJTjELMAkGA1UECAwCSFIxETAPBgNVBAcM\n" + + "CEd1cnVncmFtMQ8wDQYDVQQKDAZBcGFjaGUxEzARBgNVBAsMCkNsb3VkU3RhY2sx\n" + + "DzANBgNVBAMMBnYtMS1WTTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD46KFWKYrJ\n" + + "F43Y1oqWUfrl4mj4Qm05Bgsi6nuigZv7ufiAKK0nO4iJKdRa2hFMUvBi2/bU3IyY\n" + + "Nvg7cdJsn4K9AgMBAAGgADANBgkqhkiG9w0BAQUFAANBAIta9glu/ZSjA/ncyXix\n" + + "yDOyAKmXXxsRIsdrEuIzakUuJS7C8IG0FjUbDyIaiwWQa5x+Lt4oMqCmpNqRzaGP\n" + + "fOo=\n" + "-----END CERTIFICATE REQUEST-----"); + } + + @Override + public Answer setupCertificate(SetupCertificateCommand cmd) { + return new SetupCertificateAnswer(true); + } + @Override public boolean start() { for (Discoverer discoverer : discoverers) { diff --git a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/SimulatorManagerImpl.java b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/SimulatorManagerImpl.java index 03593392ba7e..b20bd3d8034c 100644 --- a/plugins/hypervisors/simulator/src/com/cloud/agent/manager/SimulatorManagerImpl.java +++ b/plugins/hypervisors/simulator/src/com/cloud/agent/manager/SimulatorManagerImpl.java @@ -26,6 +26,8 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.ca.SetupKeyStoreCommand; import org.apache.cloudstack.storage.command.DeleteCommand; import org.apache.cloudstack.storage.command.DownloadCommand; import org.apache.cloudstack.storage.command.DownloadProgressCommand; @@ -280,6 +282,10 @@ public Answer simulate(final Command cmd, final String hostGuid) { answer = _mockAgentMgr.checkHealth((CheckHealthCommand)cmd); } else if (cmd instanceof PingTestCommand) { answer = _mockAgentMgr.pingTest((PingTestCommand)cmd); + } else if (cmd instanceof SetupKeyStoreCommand) { + answer = _mockAgentMgr.setupKeyStore((SetupKeyStoreCommand)cmd); + } else if (cmd instanceof SetupCertificateCommand) { + answer = _mockAgentMgr.setupCertificate((SetupCertificateCommand)cmd); } else if (cmd instanceof PrepareForMigrationCommand) { answer = _mockVmMgr.prepareForMigrate((PrepareForMigrationCommand)cmd); } else if (cmd instanceof MigrateCommand) { diff --git a/plugins/pom.xml b/plugins/pom.xml index 4045349d715a..bcc7240d02f8 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -53,6 +53,7 @@ acl/dynamic-role-based affinity-group-processors/host-anti-affinity affinity-group-processors/explicit-dedication + ca/root-ca deployment-planners/user-concentrated-pod deployment-planners/user-dispersing deployment-planners/implicit-dedication diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java index e2d1b8884603..d280ed5adaf8 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java @@ -16,13 +16,34 @@ // under the License. package org.apache.cloudstack.saml; -import com.cloud.domain.Domain; -import com.cloud.user.DomainManager; -import com.cloud.user.User; -import com.cloud.user.UserVO; -import com.cloud.user.dao.UserDao; -import com.cloud.utils.PropertiesUtil; -import com.cloud.utils.component.AdapterBase; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutput; +import java.io.ObjectOutputStream; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +import javax.inject.Inject; +import javax.xml.stream.FactoryConfigurationError; + import org.apache.cloudstack.api.command.AuthorizeSAMLSSOCmd; import org.apache.cloudstack.api.command.GetServiceProviderMetaDataCmd; import org.apache.cloudstack.api.command.ListAndSwitchSAMLAccountCmd; @@ -34,9 +55,11 @@ import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.security.keystore.KeystoreDao; import org.apache.cloudstack.framework.security.keystore.KeystoreVO; +import org.apache.cloudstack.utils.security.CertUtils; import org.apache.commons.codec.binary.Base64; import org.apache.commons.httpclient.HttpClient; import org.apache.log4j.Logger; +import org.bouncycastle.operator.OperatorCreationException; import org.opensaml.DefaultBootstrap; import org.opensaml.common.xml.SAMLConstants; import org.opensaml.saml2.metadata.ContactPerson; @@ -61,32 +84,13 @@ import org.opensaml.xml.security.keyinfo.KeyInfoHelper; import org.springframework.stereotype.Component; -import javax.inject.Inject; -import javax.xml.stream.FactoryConfigurationError; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutput; -import java.io.ObjectOutputStream; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SignatureException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Timer; -import java.util.TimerTask; +import com.cloud.domain.Domain; +import com.cloud.user.DomainManager; +import com.cloud.user.User; +import com.cloud.user.UserVO; +import com.cloud.user.dao.UserDao; +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.component.AdapterBase; @Component public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManager, Configurable { @@ -141,12 +145,14 @@ protected boolean initSP() { KeystoreVO keyStoreVO = _ksDao.findByName(SAMLPluginConstants.SAMLSP_KEYPAIR); if (keyStoreVO == null) { try { - KeyPair keyPair = SAMLUtils.generateRandomKeyPair(); - _ksDao.save(SAMLPluginConstants.SAMLSP_KEYPAIR, SAMLUtils.savePrivateKey(keyPair.getPrivate()), SAMLUtils.savePublicKey(keyPair.getPublic()), "samlsp-keypair"); + KeyPair keyPair = CertUtils.generateRandomKeyPair(4096); + _ksDao.save(SAMLPluginConstants.SAMLSP_KEYPAIR, + CertUtils.privateKeyToPem(keyPair.getPrivate()), + CertUtils.publicKeyToPem(keyPair.getPublic()), "samlsp-keypair"); keyStoreVO = _ksDao.findByName(SAMLPluginConstants.SAMLSP_KEYPAIR); s_logger.info("No SAML keystore found, created and saved a new Service Provider keypair"); - } catch (NoSuchProviderException | NoSuchAlgorithmException e) { - s_logger.error("Unable to create and save SAML keypair: " + e.toString()); + } catch (final NoSuchProviderException | NoSuchAlgorithmException | IOException e) { + s_logger.error("Unable to create and save SAML keypair, due to: ", e); } } @@ -160,8 +166,19 @@ protected boolean initSP() { KeyPair spKeyPair = null; X509Certificate spX509Key = null; if (keyStoreVO != null) { - PrivateKey privateKey = SAMLUtils.loadPrivateKey(keyStoreVO.getCertificate()); - PublicKey publicKey = SAMLUtils.loadPublicKey(keyStoreVO.getKey()); + + PrivateKey privateKey = null; + try { + privateKey = CertUtils.pemToPrivateKey(keyStoreVO.getCertificate()); + } catch (final InvalidKeySpecException | IOException e) { + s_logger.error("Failed to read private key, due to error: ", e); + } + PublicKey publicKey = null; + try { + publicKey = CertUtils.pemToPublicKey(keyStoreVO.getKey()); + } catch (final InvalidKeySpecException | IOException e) { + s_logger.error("Failed to read public key, due to error: ", e); + } if (privateKey != null && publicKey != null) { spKeyPair = new KeyPair(publicKey, privateKey); KeystoreVO x509VO = _ksDao.findByName(SAMLPluginConstants.SAMLSP_X509CERT); @@ -174,8 +191,8 @@ protected boolean initSP() { out.flush(); _ksDao.save(SAMLPluginConstants.SAMLSP_X509CERT, Base64.encodeBase64String(bos.toByteArray()), "", "samlsp-x509cert"); bos.close(); - } catch (NoSuchAlgorithmException | NoSuchProviderException | CertificateEncodingException | SignatureException | InvalidKeyException | IOException e) { - s_logger.error("SAML Plugin won't be able to use X509 signed authentication"); + } catch (final NoSuchAlgorithmException | NoSuchProviderException | CertificateException | SignatureException | InvalidKeyException | IOException | OperatorCreationException e) { + s_logger.error("SAML plugin won't be able to use X509 signed authentication", e); } } else { try { diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java index ec6b2c11e5e2..364ef86103ff 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java @@ -19,14 +19,41 @@ package org.apache.cloudstack.saml; -import com.cloud.utils.HttpUtils; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.FactoryConfigurationError; + import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.response.LoginCmdResponse; +import org.apache.cloudstack.utils.security.CertUtils; import org.apache.log4j.Logger; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.x509.X509V1CertificateGenerator; +import org.bouncycastle.operator.OperatorCreationException; import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; import org.opensaml.Configuration; import org.opensaml.DefaultBootstrap; import org.opensaml.common.SAMLVersion; @@ -63,41 +90,7 @@ import org.w3c.dom.Element; import org.xml.sax.SAXException; -import javax.security.auth.x500.X500Principal; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletResponse; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.stream.FactoryConfigurationError; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.StringWriter; -import java.io.UnsupportedEncodingException; -import java.math.BigInteger; -import java.net.URLEncoder; -import java.nio.charset.Charset; -import java.security.InvalidKeyException; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.Security; -import java.security.Signature; -import java.security.SignatureException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.List; -import java.util.zip.Deflater; -import java.util.zip.DeflaterOutputStream; +import com.cloud.utils.HttpUtils; public class SAMLUtils { public static final Logger s_logger = Logger.getLogger(SAMLUtils.class); @@ -271,89 +264,10 @@ public static String generateSAMLRequestSignature(final String urlEncodedString, return url; } - public static KeyFactory getKeyFactory() { - KeyFactory keyFactory = null; - try { - Security.addProvider(new BouncyCastleProvider()); - keyFactory = KeyFactory.getInstance("RSA", "BC"); - } catch (NoSuchAlgorithmException | NoSuchProviderException e) { - s_logger.error("Unable to create KeyFactory:" + e.getMessage()); - } - return keyFactory; - } - - public static String savePublicKey(PublicKey key) { - try { - KeyFactory keyFactory = SAMLUtils.getKeyFactory(); - if (keyFactory == null) return null; - X509EncodedKeySpec spec = keyFactory.getKeySpec(key, X509EncodedKeySpec.class); - return new String(org.bouncycastle.util.encoders.Base64.encode(spec.getEncoded()), Charset.forName("UTF-8")); - } catch (InvalidKeySpecException e) { - s_logger.error("Unable to create KeyFactory:" + e.getMessage()); - } - return null; - } - - public static String savePrivateKey(PrivateKey key) { - try { - KeyFactory keyFactory = SAMLUtils.getKeyFactory(); - if (keyFactory == null) return null; - PKCS8EncodedKeySpec spec = keyFactory.getKeySpec(key, - PKCS8EncodedKeySpec.class); - return new String(org.bouncycastle.util.encoders.Base64.encode(spec.getEncoded()), Charset.forName("UTF-8")); - } catch (InvalidKeySpecException e) { - s_logger.error("Unable to create KeyFactory:" + e.getMessage()); - } - return null; - } - - public static PublicKey loadPublicKey(String publicKey) { - byte[] sigBytes = org.bouncycastle.util.encoders.Base64.decode(publicKey); - X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(sigBytes); - KeyFactory keyFact = SAMLUtils.getKeyFactory(); - if (keyFact == null) - return null; - try { - return keyFact.generatePublic(x509KeySpec); - } catch (InvalidKeySpecException e) { - s_logger.error("Unable to create PrivateKey from privateKey string:" + e.getMessage()); - } - return null; - } - - public static PrivateKey loadPrivateKey(String privateKey) { - byte[] sigBytes = org.bouncycastle.util.encoders.Base64.decode(privateKey); - PKCS8EncodedKeySpec pkscs8KeySpec = new PKCS8EncodedKeySpec(sigBytes); - KeyFactory keyFact = SAMLUtils.getKeyFactory(); - if (keyFact == null) - return null; - try { - return keyFact.generatePrivate(pkscs8KeySpec); - } catch (InvalidKeySpecException e) { - s_logger.error("Unable to create PrivateKey from privateKey string:" + e.getMessage()); - } - return null; - } - - public static KeyPair generateRandomKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException { - Security.addProvider(new BouncyCastleProvider()); - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); - keyPairGenerator.initialize(4096, new SecureRandom()); - return keyPairGenerator.generateKeyPair(); - } - - public static X509Certificate generateRandomX509Certificate(KeyPair keyPair) throws NoSuchAlgorithmException, NoSuchProviderException, CertificateEncodingException, SignatureException, InvalidKeyException { - DateTime now = DateTime.now(DateTimeZone.UTC); - X500Principal dnName = new X500Principal("CN=ApacheCloudStack"); - X509V1CertificateGenerator certGen = new X509V1CertificateGenerator(); - certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); - certGen.setSubjectDN(dnName); - certGen.setIssuerDN(dnName); - certGen.setNotBefore(now.minusDays(1).toDate()); - certGen.setNotAfter(now.plusYears(3).toDate()); - certGen.setPublicKey(keyPair.getPublic()); - certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); - return certGen.generate(keyPair.getPrivate(), "BC"); + public static X509Certificate generateRandomX509Certificate(KeyPair keyPair) throws NoSuchAlgorithmException, NoSuchProviderException, CertificateException, SignatureException, InvalidKeyException, OperatorCreationException { + return CertUtils.generateV1Certificate(keyPair, + "CN=ApacheCloudStack", "CN=ApacheCloudStack", + 3, "SHA256WithRSA"); } public static void setupSamlUserCookies(final LoginCmdResponse loginResponse, final HttpServletResponse resp) throws IOException { diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java index 86009ac4e5c1..3df0fccadfad 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java @@ -19,13 +19,22 @@ package org.apache.cloudstack; -import com.cloud.utils.HttpUtils; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.security.KeyPair; +import java.security.cert.X509Certificate; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + import org.apache.cloudstack.api.ApiServerService; import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.api.command.GetServiceProviderMetaDataCmd; import org.apache.cloudstack.saml.SAML2AuthManager; import org.apache.cloudstack.saml.SAMLProviderMetadata; import org.apache.cloudstack.saml.SAMLUtils; +import org.apache.cloudstack.utils.security.CertUtils; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,20 +42,7 @@ import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import java.lang.reflect.Field; -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.SignatureException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.net.InetAddress; -import java.net.UnknownHostException; +import com.cloud.utils.HttpUtils; @RunWith(MockitoJUnitRunner.class) public class GetServiceProviderMetaDataCmdTest { @@ -67,7 +63,7 @@ public class GetServiceProviderMetaDataCmdTest { HttpServletRequest req; @Test - public void testAuthenticate() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, CertificateParsingException, CertificateEncodingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException, UnknownHostException { + public void testAuthenticate() throws Exception { GetServiceProviderMetaDataCmd cmd = new GetServiceProviderMetaDataCmd(); Field apiServerField = GetServiceProviderMetaDataCmd.class.getDeclaredField("_apiServer"); @@ -80,7 +76,7 @@ public void testAuthenticate() throws NoSuchFieldException, SecurityException, I String spId = "someSPID"; String url = "someUrl"; - KeyPair kp = SAMLUtils.generateRandomKeyPair(); + KeyPair kp = CertUtils.generateRandomKeyPair(4096); X509Certificate cert = SAMLUtils.generateRandomX509Certificate(kp); SAMLProviderMetadata providerMetadata = new SAMLProviderMetadata(); diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java index bd87831913c2..4986d7a2b317 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java @@ -19,15 +19,17 @@ package org.apache.cloudstack; -import junit.framework.TestCase; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; + import org.apache.cloudstack.saml.SAMLUtils; +import org.apache.cloudstack.utils.security.CertUtils; import org.junit.Test; import org.opensaml.saml2.core.AuthnRequest; import org.opensaml.saml2.core.LogoutRequest; -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.PublicKey; +import junit.framework.TestCase; public class SAMLUtilsTest extends TestCase { @@ -60,13 +62,13 @@ public void testBuildLogoutRequest() throws Exception { @Test public void testX509Helpers() throws Exception { - KeyPair keyPair = SAMLUtils.generateRandomKeyPair(); + KeyPair keyPair = CertUtils.generateRandomKeyPair(4096); - String privateKeyString = SAMLUtils.savePrivateKey(keyPair.getPrivate()); - String publicKeyString = SAMLUtils.savePublicKey(keyPair.getPublic()); + String privateKeyString = CertUtils.privateKeyToPem(keyPair.getPrivate()); + String publicKeyString = CertUtils.publicKeyToPem(keyPair.getPublic()); - PrivateKey privateKey = SAMLUtils.loadPrivateKey(privateKeyString); - PublicKey publicKey = SAMLUtils.loadPublicKey(publicKeyString); + PrivateKey privateKey = CertUtils.pemToPrivateKey(privateKeyString); + PublicKey publicKey = CertUtils.pemToPublicKey(publicKeyString); assertTrue(privateKey.equals(keyPair.getPrivate())); assertTrue(publicKey.equals(keyPair.getPublic())); diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java index 36140f2bedd5..2ce88414b409 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java @@ -21,12 +21,16 @@ import static org.junit.Assert.assertFalse; -import com.cloud.domain.Domain; -import com.cloud.user.AccountService; -import com.cloud.user.DomainManager; -import com.cloud.user.UserAccountVO; -import com.cloud.user.dao.UserAccountDao; -import com.cloud.utils.HttpUtils; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; import org.apache.cloudstack.api.ApiServerService; import org.apache.cloudstack.api.BaseCmd; @@ -36,6 +40,7 @@ import org.apache.cloudstack.saml.SAMLPluginConstants; import org.apache.cloudstack.saml.SAMLProviderMetadata; import org.apache.cloudstack.saml.SAMLUtils; +import org.apache.cloudstack.utils.security.CertUtils; import org.joda.time.DateTime; import org.junit.Assert; import org.junit.Test; @@ -64,16 +69,12 @@ import org.opensaml.saml2.core.impl.StatusCodeBuilder; import org.opensaml.saml2.core.impl.SubjectBuilder; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -import java.lang.reflect.Field; -import java.security.KeyPair; -import java.security.cert.X509Certificate; -import java.util.HashMap; -import java.util.Map; -import java.net.InetAddress; +import com.cloud.domain.Domain; +import com.cloud.user.AccountService; +import com.cloud.user.DomainManager; +import com.cloud.user.UserAccountVO; +import com.cloud.user.dao.UserAccountDao; +import com.cloud.utils.HttpUtils; @RunWith(MockitoJUnitRunner.class) public class SAML2LoginAPIAuthenticatorCmdTest { @@ -158,7 +159,7 @@ public void testAuthenticate() throws Exception { userAccountDaoField.setAccessible(true); userAccountDaoField.set(cmd, userAccountDao); - KeyPair kp = SAMLUtils.generateRandomKeyPair(); + KeyPair kp = CertUtils.generateRandomKeyPair(4096); X509Certificate cert = SAMLUtils.generateRandomX509Certificate(kp); SAMLProviderMetadata providerMetadata = new SAMLProviderMetadata(); diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java index cbfcc55c540b..09391c5d7d21 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java @@ -19,11 +19,19 @@ package org.apache.cloudstack.api.command; -import com.cloud.utils.HttpUtils; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.security.cert.X509Certificate; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + import org.apache.cloudstack.api.ApiServerService; import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.saml.SAML2AuthManager; import org.apache.cloudstack.saml.SAMLUtils; +import org.apache.cloudstack.utils.security.CertUtils; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -31,12 +39,7 @@ import org.mockito.Mockito; import org.mockito.runners.MockitoJUnitRunner; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import java.lang.reflect.Field; -import java.security.cert.X509Certificate; -import java.net.InetAddress; +import com.cloud.utils.HttpUtils; @RunWith(MockitoJUnitRunner.class) public class SAML2LogoutAPIAuthenticatorCmdTest { @@ -70,7 +73,7 @@ public void testAuthenticate() throws Exception { String spId = "someSPID"; String url = "someUrl"; - X509Certificate cert = SAMLUtils.generateRandomX509Certificate(SAMLUtils.generateRandomKeyPair()); + X509Certificate cert = SAMLUtils.generateRandomX509Certificate(CertUtils.generateRandomKeyPair(4096)); Mockito.when(session.getAttribute(Mockito.anyString())).thenReturn(null); cmd.authenticate("command", null, session, InetAddress.getByName("127.0.0.1"), HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp); diff --git a/pom.xml b/pom.xml index edfd649d7595..2ff63e6f455f 100644 --- a/pom.xml +++ b/pom.xml @@ -64,7 +64,7 @@ 4.12 1.3 1.12.0 - 1.55 + 1.57 0.1.54 2.1.1 1.9.2 @@ -225,6 +225,11 @@ bcprov-jdk15on ${cs.bcprov.version} + + org.bouncycastle + bcpkix-jdk15on + ${cs.bcprov.version} + org.apache.xmlgraphics batik-css diff --git a/scripts/common/keys/ssl-keys.py b/scripts/common/keys/ssl-keys.py deleted file mode 100644 index d6804cca19d5..000000000000 --- a/scripts/common/keys/ssl-keys.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - - -# Copies keys that enable SSH communication with system vms -# $1 = new public key -# $2 = new private key -''' -All imports go here... -''' -from subprocess import call -import socket -import sys -import os -import subprocess -import traceback - -def generateSSLKey(outputPath): - logf = open("ssl-keys.log", "w") - hostName = socket.gethostbyname(socket.gethostname()) - keyFile = outputPath + os.sep + "cloudmanagementserver.keystore" - logf.write("HostName = %s\n" % hostName) - logf.write("OutputPath = %s\n" % keyFile) - dname='cn="Cloudstack User",ou="' + hostName + '",o="' + hostName + '",c="Unknown"'; - logf.write("dname = %s\n" % dname) - logf.flush() - try : - return_code = subprocess.Popen(["keytool", "-genkey", "-keystore", keyFile, "-storepass", "vmops.com", "-keypass", "vmops.com", "-keyalg", "RSA", "-validity", "3650", "-dname", dname],shell=True,stdout=logf, stderr=logf) - return_code.wait() - except OSError as e: - logf.flush() - traceback.print_exc(file=logf) - logf.flush() - logf.write("SSL key generated is : %s" % return_code) - logf.flush() - -argsSize=len(sys.argv) -if argsSize != 2: - print("Usage: ssl-keys.py ") - sys.exit(None) -sslKeyPath=sys.argv[1] - -generateSSLKey(sslKeyPath) \ No newline at end of file diff --git a/scripts/installer/windows/acs.wxs b/scripts/installer/windows/acs.wxs index fa8ff41a8a20..6f2aec07d975 100644 --- a/scripts/installer/windows/acs.wxs +++ b/scripts/installer/windows/acs.wxs @@ -255,9 +255,6 @@ - - - - diff --git a/scripts/network/domr/router_proxy.sh b/scripts/network/domr/router_proxy.sh index f9cb7ca0157c..945ca968d260 100755 --- a/scripts/network/domr/router_proxy.sh +++ b/scripts/network/domr/router_proxy.sh @@ -35,13 +35,11 @@ check_gw() { cert="/root/.ssh/id_rsa.cloud" -script=$1 -shift - -domRIp=$1 -shift +script="$1" +domRIp="$2" check_gw "$domRIp" -ssh -p 3922 -q -o StrictHostKeyChecking=no -i $cert root@$domRIp "/opt/cloud/bin/$script $*" +ssh -p 3922 -q -o StrictHostKeyChecking=no -i $cert root@$domRIp "/opt/cloud/bin/$script ${@:3}" + exit $? diff --git a/scripts/util/keystore-cert-import b/scripts/util/keystore-cert-import new file mode 100755 index 000000000000..bb03b6f68b4b --- /dev/null +++ b/scripts/util/keystore-cert-import @@ -0,0 +1,100 @@ +#!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +PROPS_FILE="$1" +KS_FILE="$2" +MODE="$3" +CERT_FILE="$4" +CERT=$(echo "$5" | tr '^' '\n' | tr '~' ' ') +CACERT_FILE="$6" +CACERT=$(echo "$7" | tr '^' '\n' | tr '~' ' ') +PRIVKEY_FILE="$8" +PRIVKEY=$(echo "$9" | tr '^' '\n' | tr '~' ' ') + +ALIAS="cloud" +SYSTEM_FILE="/var/cache/cloud/cmdline" + +# Find keystore password +KS_PASS=$(sed -n '/keystore.passphrase/p' "$PROPS_FILE" 2>/dev/null | sed 's/keystore.passphrase=//g' 2>/dev/null) + +if [ -z "${KS_PASS// }" ]; then + echo "Failed to find keystore passphrase from file: $PROPS_FILE, quiting!" + exit 1 +fi + +# Use a new keystore file +NEW_KS_FILE="$KS_FILE.new" + +# Import certificate +if [ ! -z "${CERT// }" ]; then + echo "$CERT" > "$CERT_FILE" +fi + +# Import ca certs +if [ ! -z "${CACERT// }" ]; then + echo "$CACERT" > "$CACERT_FILE" +fi + +# Import cacerts into the keystore +awk '/-----BEGIN CERTIFICATE-----?/{n++}{print > "cloudca." n }' "$CACERT_FILE" +for caChain in $(ls cloudca.*); do + keytool -delete -noprompt -alias "$caChain" -keystore "$NEW_KS_FILE" -storepass "$KS_PASS" > /dev/null 2>&1 || true + keytool -import -noprompt -storepass "$KS_PASS" -trustcacerts -alias "$caChain" -file "$caChain" -keystore "$NEW_KS_FILE" > /dev/null 2>&1 +done +rm -f cloudca.* + +# Import private key if available +if [ ! -z "${PRIVKEY// }" ]; then + echo "$PRIVKEY" > "$PRIVKEY_FILE" + # Re-initialize keystore when private key is provided + keytool -delete -noprompt -alias "$ALIAS" -keystore "$NEW_KS_FILE" -storepass "$KS_PASS" 2>/dev/null || true + openssl pkcs12 -export -name "$ALIAS" -in "$CERT_FILE" -inkey "$PRIVKEY_FILE" -out "$NEW_KS_FILE.p12" -password pass:"$KS_PASS" > /dev/null 2>&1 + keytool -importkeystore -srckeystore "$NEW_KS_FILE.p12" -destkeystore "$NEW_KS_FILE" -srcstoretype PKCS12 -alias "$ALIAS" -deststorepass "$KS_PASS" -destkeypass "$KS_PASS" -srcstorepass "$KS_PASS" -srckeypass "$KS_PASS" > /dev/null 2>&1 +else + # Import certificate into the keystore + keytool -import -storepass "$KS_PASS" -alias "$ALIAS" -file "$CERT_FILE" -keystore "$NEW_KS_FILE" > /dev/null 2>&1 || true + # Export private key from keystore + rm -f "$PRIVKEY_FILE" + keytool -importkeystore -srckeystore "$NEW_KS_FILE" -destkeystore "$NEW_KS_FILE.p12" -deststoretype PKCS12 -srcalias "$ALIAS" -deststorepass "$KS_PASS" -destkeypass "$KS_PASS" -srcstorepass "$KS_PASS" -srckeypass "$KS_PASS" > /dev/null 2>&1 + openssl pkcs12 -in "$NEW_KS_FILE.p12" -nodes -nocerts -nomac -password pass:"$KS_PASS" 2>/dev/null | openssl rsa -out "$PRIVKEY_FILE" > /dev/null 2>&1 +fi + +# Commit the new keystore +rm -f "$NEW_KS_FILE.p12" +mv -f "$NEW_KS_FILE" "$KS_FILE" + +# Update ca-certs if we're in systemvm +if [ -f "$SYSTEM_FILE" ]; then + mkdir -p /usr/local/share/ca-certificates/cloudstack + cp "$CACERT_FILE" /usr/local/share/ca-certificates/cloudstack/ca.crt + chmod 755 /usr/local/share/ca-certificates/cloudstack + chmod 644 /usr/local/share/ca-certificates/cloudstack/ca.crt + update-ca-certificates > /dev/null 2>&1 || true +fi + +# Restart cloud service if we're in systemvm +if [ "$MODE" == "ssh" ] && [ -f $SYSTEM_FILE ]; then + /etc/init.d/cloud stop > /dev/null 2>&1 + sleep 2 + /etc/init.d/cloud start > /dev/null 2>&1 +fi + +# Fix file permission +chmod 600 $CACERT_FILE +chmod 600 $CERT_FILE +chmod 600 $PRIVKEY_FILE diff --git a/scripts/util/keystore-setup b/scripts/util/keystore-setup new file mode 100755 index 000000000000..28ce61c846a5 --- /dev/null +++ b/scripts/util/keystore-setup @@ -0,0 +1,51 @@ +#!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +PROPS_FILE="$1" +KS_FILE="$2.new" +KS_PASS="$3" +KS_VALIDITY="$4" +CSR_FILE="$5" + +ALIAS="cloud" + +# Re-use existing password or use the one provided +if [ -f "$PROPS_FILE" ]; then + OLD_PASS=$(sed -n '/keystore.passphrase/p' "$PROPS_FILE" 2>/dev/null | sed 's/keystore.passphrase=//g' 2>/dev/null) + if [ ! -z "${OLD_PASS// }" ]; then + KS_PASS="$OLD_PASS" + else + sed -i "/keystore.passphrase.*/d" $PROPS_FILE 2> /dev/null || true + echo "keystore.passphrase=$KS_PASS" >> $PROPS_FILE + fi +fi + +# Generate keystore +rm -f "$KS_FILE" +CN=$(hostname --fqdn) +keytool -genkey -storepass "$KS_PASS" -keypass "$KS_PASS" -alias "$ALIAS" -keyalg RSA -validity "$KS_VALIDITY" -dname cn="$CN",ou="cloudstack",o="cloudstack",c="cloudstack" -keystore "$KS_FILE" + +# Generate CSR +rm -f "$CSR_FILE" +keytool -certreq -storepass "$KS_PASS" -alias "$ALIAS" -file $CSR_FILE -keystore "$KS_FILE" +cat "$CSR_FILE" + +# Fix file permissions +chmod 600 $KS_FILE +chmod 600 $PROPS_FILE +chmod 600 $CSR_FILE diff --git a/server/pom.xml b/server/pom.xml index c49b614c1a0e..4067b8590a07 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -52,6 +52,11 @@ httpcore ${cs.httpcore.version} + + org.apache.cloudstack + cloud-framework-ca + ${project.version} + org.apache.cloudstack cloud-framework-jobs diff --git a/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index 56da591cdea2..8a8d6452c23e 100644 --- a/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -87,8 +87,10 @@ + + @@ -277,4 +279,10 @@ + + + + + + diff --git a/server/src/com/cloud/alert/AlertManagerImpl.java b/server/src/com/cloud/alert/AlertManagerImpl.java index c751c6adb35e..0232843050dd 100644 --- a/server/src/com/cloud/alert/AlertManagerImpl.java +++ b/server/src/com/cloud/alert/AlertManagerImpl.java @@ -759,7 +759,8 @@ public void sendAlert(AlertType alertType, long dataCenterId, Long podId, Long c (alertType != AlertManager.AlertType.ALERT_TYPE_MANAGMENT_NODE) && (alertType != AlertManager.AlertType.ALERT_TYPE_RESOURCE_LIMIT_EXCEEDED) && (alertType != AlertManager.AlertType.ALERT_TYPE_UPLOAD_FAILED) && - (alertType != AlertManager.AlertType.ALERT_TYPE_OOBM_AUTH_ERROR)) { + (alertType != AlertManager.AlertType.ALERT_TYPE_OOBM_AUTH_ERROR) && + (alertType != AlertManager.AlertType.ALERT_TYPE_CA_CERT)) { alert = _alertDao.getLastAlert(alertType.getType(), dataCenterId, podId, clusterId); } diff --git a/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index 1817ade28744..cee07455d3a0 100644 --- a/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -110,6 +110,7 @@ import com.cloud.utils.DateUtil; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; +import com.cloud.utils.StringUtils; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.db.DB; import com.cloud.utils.db.GlobalLock; @@ -1354,7 +1355,7 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl StringBuilder buf = profile.getBootArgsBuilder(); buf.append(" template=domP type=consoleproxy"); - buf.append(" host=").append(ApiServiceConfiguration.ManagementHostIPAdr.value()); + buf.append(" host=").append(StringUtils.shuffleCSVList(ApiServiceConfiguration.ManagementHostIPAdr.value())); buf.append(" port=").append(_mgmtPort); buf.append(" name=").append(profile.getVirtualMachine().getHostName()); if (_sslEnabled) { diff --git a/server/src/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java b/server/src/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java index 13a1a64cd21a..ac5b48aa2b58 100644 --- a/server/src/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java +++ b/server/src/com/cloud/hypervisor/kvm/discoverer/LibvirtServerDiscoverer.java @@ -16,6 +16,23 @@ // under the License. package com.cloud.hypervisor.kvm.discoverer; +import java.net.InetAddress; +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.ca.CAManager; +import org.apache.cloudstack.ca.SetupCertificateCommand; +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.utils.security.KeyStoreUtils; +import org.apache.log4j.Logger; + import com.cloud.agent.AgentManager; import com.cloud.agent.Listener; import com.cloud.agent.api.AgentControlAnswer; @@ -42,17 +59,10 @@ 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.ssh.SSHCmdHelper; -import org.apache.log4j.Logger; - -import javax.inject.Inject; -import javax.naming.ConfigurationException; -import java.net.InetAddress; -import java.net.URI; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import com.trilead.ssh2.Connection; public abstract class LibvirtServerDiscoverer extends DiscovererBase implements Discoverer, Listener, ResourceStateAdapter { private static final Logger s_logger = Logger.getLogger(LibvirtServerDiscoverer.class); @@ -62,7 +72,9 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements private String _kvmPublicNic; private String _kvmGuestNic; @Inject - AgentManager _agentMgr; + private AgentManager agentMgr; + @Inject + private CAManager caManager; @Override public abstract Hypervisor.HypervisorType getHypervisorType(); @@ -125,6 +137,73 @@ public boolean processTimeout(long agentId, long seq) { return false; } + private void setupAgentSecurity(final Connection sshConnection, final String agentIp, final String agentHostname) { + if (!caManager.canProvisionCertificates()) { + s_logger.warn("Cannot secure agent communication because configure CA plugin cannot provision client certificate"); + return; + } + + if (sshConnection == null) { + s_logger.warn("Cannot secure agent communication because ssh connection is invalid for host ip=" + agentIp); + return; + } + + Integer validityPeriod = CAManager.CertValidityPeriod.value(); + if (validityPeriod < 1) { + validityPeriod = 1; + } + + final SSHCmdHelper.SSHCmdResult keystoreSetupResult = SSHCmdHelper.sshExecuteCmdWithResult(sshConnection, + String.format("/usr/share/cloudstack-common/scripts/util/%s " + + "/etc/cloudstack/agent/agent.properties " + + "/etc/cloudstack/agent/%s " + + "%s %d " + + "/etc/cloudstack/agent/%s", + KeyStoreUtils.keyStoreSetupScript, + KeyStoreUtils.defaultKeystoreFile, + PasswordGenerator.generateRandomPassword(16), + validityPeriod, + KeyStoreUtils.defaultCsrFile)); + + if (!keystoreSetupResult.isSuccess()) { + s_logger.error("Failing, the keystore setup script failed execution on the KVM host: " + agentIp); + return; + } + + final Certificate certificate = caManager.issueCertificate(keystoreSetupResult.getStdOut(), Collections.singletonList(agentHostname), Collections.singletonList(agentIp), null, null); + if (certificate == null || certificate.getClientCertificate() == null) { + s_logger.error("Failing, the configured CA plugin failed to issue certificates for KVM host agent: " + agentIp); + return; + } + + final SetupCertificateCommand certificateCommand = new SetupCertificateCommand(certificate); + final SSHCmdHelper.SSHCmdResult setupCertResult = SSHCmdHelper.sshExecuteCmdWithResult(sshConnection, + String.format("/usr/share/cloudstack-common/scripts/util/%s " + + "/etc/cloudstack/agent/agent.properties " + + "/etc/cloudstack/agent/%s %s " + + "/etc/cloudstack/agent/%s \"%s\" " + + "/etc/cloudstack/agent/%s \"%s\" " + + "/etc/cloudstack/agent/%s \"%s\"", + KeyStoreUtils.keyStoreImportScript, + KeyStoreUtils.defaultKeystoreFile, + KeyStoreUtils.sshMode, + KeyStoreUtils.defaultCertFile, + certificateCommand.getEncodedCertificate(), + KeyStoreUtils.defaultCaCertFile, + certificateCommand.getEncodedCaCertificates(), + KeyStoreUtils.defaultPrivateKeyFile, + certificateCommand.getEncodedPrivateKey())); + + if (setupCertResult != null && !setupCertResult.isSuccess()) { + s_logger.error("Failed to setup certificate in the KVM agent's keystore file, please configure manually!"); + return; + } + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Succeeded to import certificate in the keystore for agent on the KVM host: " + agentIp + ". Agent secured and trusted."); + } + } + @Override public Map> find(long dcId, Long podId, Long clusterId, URI uri, String username, String password, List hostTags) throws DiscoveryException { @@ -143,7 +222,7 @@ public boolean processTimeout(long agentId, long seq) { s_logger.debug(msg); return null; } - com.trilead.ssh2.Connection sshConnection = null; + Connection sshConnection = null; String agentIp = null; try { @@ -162,7 +241,7 @@ public boolean processTimeout(long agentId, long seq) { } } - sshConnection = new com.trilead.ssh2.Connection(agentIp, 22); + sshConnection = new Connection(agentIp, 22); sshConnection.connect(null, 60000, 60000); if (!sshConnection.authenticateWithPassword(username, password)) { @@ -170,7 +249,7 @@ public boolean processTimeout(long agentId, long seq) { throw new DiscoveredWithErrorException("Authentication error"); } - if (!SSHCmdHelper.sshExecuteCmd(sshConnection, "lsmod|grep kvm", 3)) { + if (!SSHCmdHelper.sshExecuteCmd(sshConnection, "lsmod|grep kvm")) { s_logger.debug("It's not a KVM enabled machine"); return null; } @@ -210,7 +289,9 @@ public boolean processTimeout(long agentId, long seq) { kvmGuestNic = (kvmPublicNic != null) ? kvmPublicNic : kvmPrivateNic; } - String parameters = " -m " + _hostIp + " -z " + dcId + " -p " + podId + " -c " + clusterId + " -g " + guid + " -a"; + setupAgentSecurity(sshConnection, agentIp, hostname); + + String parameters = " -m " + StringUtils.shuffleCSVList(_hostIp) + " -z " + dcId + " -p " + podId + " -c " + clusterId + " -g " + guid + " -a"; parameters += " --pubNic=" + kvmPublicNic; parameters += " --prvNic=" + kvmPrivateNic; @@ -221,8 +302,7 @@ public boolean processTimeout(long agentId, long seq) { if (!username.equals("root")) { setupAgentCommand = "sudo cloudstack-setup-agent "; } - if (!SSHCmdHelper.sshExecuteCmd(sshConnection, - setupAgentCommand + parameters, 3)) { + if (!SSHCmdHelper.sshExecuteCmd(sshConnection, setupAgentCommand + parameters)) { s_logger.info("cloudstack agent setup command failed: " + setupAgentCommand + parameters); return null; @@ -392,7 +472,7 @@ public DeleteHostAnswer deleteHost(HostVO host, boolean isForced, boolean isForc _resourceMgr.deleteRoutingHost(host, isForced, isForceDeleteStorage); try { ShutdownCommand cmd = new ShutdownCommand(ShutdownCommand.DeleteHost, null); - _agentMgr.send(host.getId(), cmd); + agentMgr.send(host.getId(), cmd); } catch (AgentUnavailableException e) { s_logger.warn("Sending ShutdownCommand failed: ", e); } catch (OperationTimedoutException e) { diff --git a/server/src/com/cloud/resource/ResourceManagerImpl.java b/server/src/com/cloud/resource/ResourceManagerImpl.java index af5344c8e62b..5eee2469bbbb 100755 --- a/server/src/com/cloud/resource/ResourceManagerImpl.java +++ b/server/src/com/cloud/resource/ResourceManagerImpl.java @@ -2272,7 +2272,8 @@ private boolean doCancelMaintenance(final long hostId) { } try { - SSHCmdHelper.sshExecuteCmdOneShot(connection, "service cloudstack-agent restart"); + SSHCmdHelper.SSHCmdResult result = SSHCmdHelper.sshExecuteCmdOneShot(connection, "service cloudstack-agent restart"); + s_logger.debug("cloudstack-agent restart result: " + result.toString()); } catch (final SshException e) { return false; } diff --git a/server/src/com/cloud/server/ConfigurationServerImpl.java b/server/src/com/cloud/server/ConfigurationServerImpl.java index c94d92c85dc3..27172b47bbc8 100644 --- a/server/src/com/cloud/server/ConfigurationServerImpl.java +++ b/server/src/com/cloud/server/ConfigurationServerImpl.java @@ -23,8 +23,6 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; import java.security.NoSuchAlgorithmException; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -36,24 +34,21 @@ import java.util.Properties; import java.util.Set; import java.util.UUID; -import java.util.regex.Pattern; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.inject.Inject; import javax.naming.ConfigurationException; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.log4j.Logger; - import org.apache.cloudstack.config.ApiServiceConfiguration; import org.apache.cloudstack.framework.config.ConfigDepot; import org.apache.cloudstack.framework.config.ConfigDepotAdmin; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.config.impl.ConfigurationVO; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; import com.cloud.configuration.Config; import com.cloud.configuration.ConfigurationManager; @@ -117,7 +112,6 @@ import com.cloud.utils.db.TransactionStatus; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; -import com.cloud.utils.nio.Link; import com.cloud.utils.script.Script; public class ConfigurationServerImpl extends ManagerBase implements ConfigurationServer { @@ -306,9 +300,6 @@ public void doInTransactionWithoutResult(TransactionStatus status) { // Update resource count if needed updateResourceCount(); - // keystore for SSL/TLS connection - updateSSLKeystore(); - // store the public and private keys in the database updateKeyPairs(); @@ -544,117 +535,6 @@ protected void updateCloudIdentifier() { } } - static String getBase64Keystore(String keystorePath) throws IOException { - byte[] storeBytes = FileUtils.readFileToByteArray(new File(keystorePath)); - if (storeBytes.length > 3000) { // Base64 codec would enlarge data by 1/3, and we have 4094 bytes in database entry at most - throw new IOException("KeyStore is too big for database! Length " + storeBytes.length); - } - - return new String(Base64.encodeBase64(storeBytes)); - } - - private void generateDefaultKeystore(String keystorePath) throws IOException { - String cn = "Cloudstack User"; - String ou; - - try { - ou = InetAddress.getLocalHost().getCanonicalHostName(); - String[] group = ou.split("\\."); - - // Simple check to see if we got IP Address... - boolean isIPAddress = Pattern.matches("[0-9]$", group[group.length - 1]); - if (isIPAddress) { - ou = "cloud.com"; // leaving this example reference to cloud.com as it has no real world relevance - } else { - ou = group[group.length - 1]; - for (int i = group.length - 2; i >= 0 && i >= group.length - 3; i--) - ou = group[i] + "." + ou; - } - } catch (UnknownHostException ex) { - s_logger.info("Fail to get user's domain name. Would use cloud.com. ", ex); - ou = "cloud.com"; // leaving this example reference to cloud.com as it has no real world relevance - } - - String o = ou; - String c = "Unknown"; - String dname = "cn=\"" + cn + "\",ou=\"" + ou + "\",o=\"" + o + "\",c=\"" + c + "\""; - Script script = new Script(true, "keytool", 5000, null); - script.add("-genkey"); - script.add("-keystore", keystorePath); - script.add("-storepass", "vmops.com"); - script.add("-keypass", "vmops.com"); - script.add("-keyalg", "RSA"); - script.add("-validity", "3650"); - script.add("-dname", dname); - String result = script.execute(); - if (result != null) { - throw new IOException("Fail to generate certificate!: " + result); - } - } - - protected void updateSSLKeystore() { - if (s_logger.isInfoEnabled()) { - s_logger.info("Processing updateSSLKeyStore"); - } - - String dbString = _configDao.getValue("ssl.keystore"); - - File confFile = PropertiesUtil.findConfigFile("db.properties"); - String confPath = null; - String keystorePath = null; - File keystoreFile = null; - - if (null != confFile) { - confPath = confFile.getParent(); - keystorePath = confPath + Link.keystoreFile; - keystoreFile = new File(keystorePath); - } - - boolean dbExisted = (dbString != null && !dbString.isEmpty()); - - s_logger.info("SSL keystore located at " + keystorePath); - try { - if (!dbExisted && null != confFile) { - if (!keystoreFile.exists()) { - generateDefaultKeystore(keystorePath); - s_logger.info("Generated SSL keystore."); - } - String base64Keystore = getBase64Keystore(keystorePath); - ConfigurationVO configVO = - new ConfigurationVO("Hidden", "DEFAULT", "management-server", "ssl.keystore", base64Keystore, - "SSL Keystore for the management servers"); - _configDao.persist(configVO); - s_logger.info("Stored SSL keystore to database."); - } else { // !keystoreFile.exists() and dbExisted - // Export keystore to local file - byte[] storeBytes = Base64.decodeBase64(dbString); - String tmpKeystorePath = "/tmp/tmpkey"; - try ( - FileOutputStream fo = new FileOutputStream(tmpKeystorePath); - ) { - fo.write(storeBytes); - Script script = new Script(true, "cp", 5000, null); - script.add("-f"); - script.add(tmpKeystorePath); - - //There is a chance, although small, that the keystorePath is null. In that case, do not add it to the script. - if (null != keystorePath) { - script.add(keystorePath); - } - String result = script.execute(); - if (result != null) { - throw new IOException(); - } - } catch (Exception e) { - throw new IOException("Fail to create keystore file!", e); - } - s_logger.info("Stored database keystore to local."); - } - } catch (Exception ex) { - s_logger.warn("Would use fail-safe keystore to continue.", ex); - } - } - @DB protected void updateSystemvmPassword() { String userid = System.getProperty("user.name"); diff --git a/server/src/org/apache/cloudstack/ca/CAManagerImpl.java b/server/src/org/apache/cloudstack/ca/CAManagerImpl.java new file mode 100644 index 000000000000..b8b752de6512 --- /dev/null +++ b/server/src/org/apache/cloudstack/ca/CAManagerImpl.java @@ -0,0 +1,428 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.ca; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +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; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.ca.IssueCertificateCmd; +import org.apache.cloudstack.api.command.admin.ca.ListCAProvidersCmd; +import org.apache.cloudstack.api.command.admin.ca.ListCaCertificateCmd; +import org.apache.cloudstack.api.command.admin.ca.ProvisionCertificateCmd; +import org.apache.cloudstack.api.command.admin.ca.RevokeCertificateCmd; +import org.apache.cloudstack.context.CallContext; +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.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.poll.BackgroundPollManager; +import org.apache.cloudstack.poll.BackgroundPollTask; +import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.cloudstack.utils.security.CertUtils; +import org.apache.log4j.Logger; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import com.cloud.agent.AgentManager; +import com.cloud.alert.AlertManager; +import com.cloud.certificate.CrlVO; +import com.cloud.certificate.dao.CrlDao; +import com.cloud.event.ActionEvent; +import com.cloud.event.EventTypes; +import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.OperationTimedoutException; +import com.cloud.host.Host; +import com.cloud.host.Status; +import com.cloud.host.dao.HostDao; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.exception.CloudRuntimeException; +import com.google.common.base.Strings; + +public class CAManagerImpl extends ManagerBase implements CAManager { + public static final Logger LOG = Logger.getLogger(CAManagerImpl.class); + + @Inject + private CrlDao crlDao; + @Inject + private HostDao hostDao; + @Inject + private AgentManager agentManager; + @Inject + private BackgroundPollManager backgroundPollManager; + @Inject + private AlertManager alertManager; + + private static CAProvider configuredCaProvider; + private static Map caProviderMap = new HashMap<>(); + private static Map alertMap = new ConcurrentHashMap<>(); + private static Map activeCertMap = new ConcurrentHashMap<>(); + + private List caProviders; + + private CAProvider getConfiguredCaProvider() { + if (configuredCaProvider != null) { + return configuredCaProvider; + } + if (caProviderMap.containsKey(CAProviderPlugin.value()) && caProviderMap.get(CAProviderPlugin.value()) != null) { + configuredCaProvider = caProviderMap.get(CAProviderPlugin.value()); + return configuredCaProvider; + } + throw new CloudRuntimeException("Failed to find default configured CA provider plugin"); + } + + private CAProvider getCAProvider(final String provider) { + if (Strings.isNullOrEmpty(provider)) { + return getConfiguredCaProvider(); + } + final String caProviderName = provider.toLowerCase(); + if (!caProviderMap.containsKey(caProviderName)) { + throw new CloudRuntimeException(String.format("CA provider plugin '%s' not found", caProviderName)); + } + final CAProvider caProvider = caProviderMap.get(caProviderName); + if (caProvider == null) { + throw new CloudRuntimeException(String.format("CA provider plugin '%s' returned is null", caProviderName)); + } + return caProvider; + } + + /////////////////////////////////////////////////////////// + /////////////// CA Manager API Handlers /////////////////// + /////////////////////////////////////////////////////////// + + @Override + public List getCaProviders() { + return caProviders; + } + + @Override + public Map getActiveCertificatesMap() { + return activeCertMap; + } + + @Override + public boolean canProvisionCertificates() { + return getConfiguredCaProvider().canProvisionCertificates(); + } + + @Override + public String getCaCertificate(final String caProvider) throws IOException { + final CAProvider provider = getCAProvider(caProvider); + return CertUtils.x509CertificatesToPem(provider.getCaCertificate()); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_CA_CERTIFICATE_ISSUE, eventDescription = "issuing certificate", async = true) + public Certificate issueCertificate(final String csr, final List domainNames, final List ipAddresses, final Integer validityDuration, final String caProvider) { + CallContext.current().setEventDetails("domain(s): " + domainNames + " addresses: " + ipAddresses); + final CAProvider provider = getCAProvider(caProvider); + Integer validity = CAManager.CertValidityPeriod.value(); + if (validityDuration != null) { + validity = validityDuration; + } + if (Strings.isNullOrEmpty(csr)) { + if (domainNames == null || domainNames.isEmpty()) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "No domains or CSR provided"); + } + return provider.issueCertificate(domainNames, ipAddresses, validity); + } + return provider.issueCertificate(csr, domainNames, ipAddresses, validity); + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_CA_CERTIFICATE_REVOKE, eventDescription = "revoking certificate", async = true) + public boolean revokeCertificate(final BigInteger certSerial, final String certCn, final String caProvider) { + CallContext.current().setEventDetails("cert serial: " + certSerial); + final CrlVO crl = crlDao.revokeCertificate(certSerial, certCn); + if (crl != null && crl.getCertSerial().equals(certSerial)) { + final CAProvider provider = getCAProvider(caProvider); + return provider.revokeCertificate(certSerial, certCn); + } + return false; + } + + @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) { + if (host == null) { + throw new CloudRuntimeException("Unable to find valid host to renew certificate for"); + } + CallContext.current().setEventDetails("host id: " + host.getId()); + CallContext.current().putContextParameter(Host.class, host.getUuid()); + final String csr; + try { + csr = generateKeyStoreAndCsr(host, null); + if (Strings.isNullOrEmpty(csr)) { + return false; + } + final Certificate certificate = issueCertificate(csr, Collections.singletonList(host.getName()), Arrays.asList(host.getPrivateIpAddress(), host.getPublicIpAddress(), host.getStorageIpAddress()), CAManager.CertValidityPeriod.value(), caProvider); + return deployCertificate(host, certificate, reconnect, null); + } catch (final AgentUnavailableException | OperationTimedoutException e) { + LOG.error("Host/agent is not available or operation timed out, failed to setup keystore and generate CSR for host/agent id=" + host.getId() + ", due to: ", e); + throw new CloudRuntimeException("Failed to generate keystore and get CSR from the host/agent id=" + host.getId()); + } + } + + @Override + public String generateKeyStoreAndCsr(final Host host, final Map sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException { + final SetupKeyStoreCommand cmd = new SetupKeyStoreCommand(CertValidityPeriod.value()); + if (sshAccessDetails != null && !sshAccessDetails.isEmpty()) { + cmd.setAccessDetail(sshAccessDetails); + } + CallContext.current().setEventDetails("generating keystore and CSR for host id: " + host.getId()); + final SetupKeystoreAnswer answer = (SetupKeystoreAnswer) agentManager.send(host.getId(), cmd); + return answer.getCsr(); + } + + @Override + public boolean deployCertificate(final Host host, final Certificate certificate, final Boolean reconnect, final Map sshAccessDetails) throws AgentUnavailableException, OperationTimedoutException { + final SetupCertificateCommand cmd = new SetupCertificateCommand(certificate); + if (sshAccessDetails != null && !sshAccessDetails.isEmpty()) { + cmd.setAccessDetail(sshAccessDetails); + } + CallContext.current().setEventDetails("deploying certificate for host id: " + host.getId()); + final SetupCertificateAnswer answer = (SetupCertificateAnswer) agentManager.send(host.getId(), cmd); + if (answer.getResult()) { + CallContext.current().setEventDetails("successfully deployed certificate for host id: " + host.getId()); + } else { + CallContext.current().setEventDetails("failed to deploy certificate for host id: " + host.getId()); + } + + if (answer.getResult()) { + getActiveCertificatesMap().put(host.getPrivateIpAddress(), certificate.getClientCertificate()); + if (sshAccessDetails == null && reconnect != null && reconnect) { + LOG.info(String.format("Successfully setup certificate on host, reconnecting with agent with id=%d, name=%s, address=%s", + host.getId(), host.getName(), host.getPublicIpAddress())); + return agentManager.reconnect(host.getId()); + } + return true; + } + return false; + } + + @Override + public void purgeHostCertificate(final Host host) { + if (host == null) { + return; + } + final String privateAddress = host.getPrivateIpAddress(); + final String publicAddress = host.getPublicIpAddress(); + final Map activeCertsMap = getActiveCertificatesMap(); + if (!Strings.isNullOrEmpty(privateAddress) && activeCertsMap.containsKey(privateAddress)) { + activeCertsMap.remove(privateAddress); + } + if (!Strings.isNullOrEmpty(publicAddress) && activeCertsMap.containsKey(publicAddress)) { + activeCertsMap.remove(publicAddress); + } + } + + @Override + public void sendAlert(final Host host, final String subject, final String message) { + if (host == null) { + return; + } + alertManager.sendAlert(AlertManager.AlertType.ALERT_TYPE_CA_CERT, + host.getDataCenterId(), host.getPodId(), subject, message); + } + + @Override + public SSLEngine createSSLEngine(final SSLContext sslContext, final String remoteAddress) throws GeneralSecurityException, IOException { + if (sslContext == null) { + throw new CloudRuntimeException("SSLContext provided to create SSLEngine is null, aborting"); + } + if (Strings.isNullOrEmpty(remoteAddress)) { + throw new CloudRuntimeException("Remote client address connecting to mgmt server cannot be empty/null"); + } + return getConfiguredCaProvider().createSSLEngine(sslContext, remoteAddress, getActiveCertificatesMap()); + } + + //////////////////////////////////////////////////// + /////////////// CA Manager Setup /////////////////// + //////////////////////////////////////////////////// + + public static final class CABackgroundTask extends ManagedContextRunnable implements BackgroundPollTask { + private CAManager caManager; + private HostDao hostDao; + + public CABackgroundTask(final CAManager caManager, final HostDao hostDao) { + this.caManager = caManager; + this.hostDao = hostDao; + } + + @Override + protected void runInContext() { + try { + if (LOG.isTraceEnabled()) { + LOG.trace("CA background task is running..."); + } + final DateTime now = DateTime.now(DateTimeZone.UTC); + final Map certsMap = caManager.getActiveCertificatesMap(); + for (final Iterator> it = certsMap.entrySet().iterator(); it.hasNext(); ) { + final Map.Entry entry = it.next(); + if (entry == null) { + continue; + } + final String hostIp = entry.getKey(); + final X509Certificate certificate = entry.getValue(); + if (certificate == null) { + it.remove(); + continue; + } + final Host host = hostDao.findByIp(hostIp); + if (host == null || host.getManagementServerId() == null || + host.getManagementServerId() != ManagementServerNode.getManagementServerId() || + host.getStatus() != Status.Up) { + if (host == null || + (host.getManagementServerId() != null && + host.getManagementServerId() != ManagementServerNode.getManagementServerId())) { + it.remove(); + } + continue; + } + + final String hostDescription = String.format("host id=%d, uuid=%s, name=%s, ip=%s, zone id=%d", + host.getId(), host.getUuid(), host.getName(), hostIp, host.getDataCenterId()); + + try { + certificate.checkValidity(now.plusDays(CertExpiryAlertPeriod.valueIn(host.getClusterId())).toDate()); + } catch (final CertificateExpiredException | CertificateNotYetValidException e) { + LOG.warn("Certificate is going to expire for " + hostDescription); + if (AutomaticCertRenewal.valueIn(host.getClusterId())) { + try { + LOG.debug("Attempting certificate auto-renewal for " + hostDescription); + boolean result = caManager.provisionCertificate(host, false, null); + if (result) { + LOG.debug("Succeeded in auto-renewing certificate for " + hostDescription); + } else { + LOG.debug("Failed in auto-renewing certificate for " + hostDescription); + } + } catch (final Throwable ex) { + LOG.warn("Failed to auto-renew certificate for " + hostDescription + ", with error=", ex); + caManager.sendAlert(host, "Certificate auto-renewal failed for " + hostDescription, + String.format("Certificate is going to expire for %s. Auto-renewal failed to renew the certificate, please renew it manually. It is not valid after %s.", hostDescription, certificate.getNotAfter())); + } + } else { + if (alertMap.containsKey(hostIp)) { + final Date lastSentDate = alertMap.get(hostIp); + if (now.minusDays(1).toDate().before(lastSentDate)) { + continue; + } + } + caManager.sendAlert(host, "Certificate expiring soon for " + hostDescription, + String.format("Certificate is going to expire for %s. Please renew it, it is not valid after %s.", + hostDescription, certificate.getNotAfter())); + alertMap.put(hostIp, new Date()); + } + } + } + } catch (final Throwable t) { + LOG.error("Error trying to run CA background task", t); + } + } + + @Override + public Long getDelay() { + return CABackgroundJobDelay.value() * 1000L; + } + } + + public void setCaProviders(final List caProviders) { + this.caProviders = caProviders; + initializeCaProviderMap(); + } + + private void initializeCaProviderMap() { + if (caProviderMap != null && caProviderMap.size() != caProviders.size()) { + for (final CAProvider caProvider : caProviders) { + caProviderMap.put(caProvider.getProviderName().toLowerCase(), caProvider); + } + } + } + + @Override + public boolean start() { + super.start(); + initializeCaProviderMap(); + if (caProviderMap.containsKey(CAProviderPlugin.value())) { + configuredCaProvider = caProviderMap.get(CAProviderPlugin.value()); + } + if (configuredCaProvider == null) { + LOG.error("Failed to find valid configured CA provider, please check!"); + return false; + } + return true; + } + + @Override + public boolean configure(final String name, final Map params) throws ConfigurationException { + backgroundPollManager.submitTask(new CABackgroundTask(this, hostDao)); + return true; + } + + ////////////////////////////////////////////////////////// + /////////////// CA Manager Descriptors /////////////////// + ////////////////////////////////////////////////////////// + + @Override + public List> getCommands() { + final List> cmdList = new ArrayList>(); + cmdList.add(ListCAProvidersCmd.class); + cmdList.add(ListCaCertificateCmd.class); + cmdList.add(IssueCertificateCmd.class); + cmdList.add(ProvisionCertificateCmd.class); + cmdList.add(RevokeCertificateCmd.class); + return cmdList; + } + + @Override + public String getConfigComponentName() { + return CAManager.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[]{ + CAProviderPlugin, + CertKeySize, + CertSignatureAlgorithm, + CertValidityPeriod, + AutomaticCertRenewal, + CABackgroundJobDelay, + CertExpiryAlertPeriod + }; + } +} diff --git a/server/src/org/apache/cloudstack/outofbandmanagement/OutOfBandManagementServiceImpl.java b/server/src/org/apache/cloudstack/outofbandmanagement/OutOfBandManagementServiceImpl.java index 4f0ac74c916c..cb6ac106bfaf 100644 --- a/server/src/org/apache/cloudstack/outofbandmanagement/OutOfBandManagementServiceImpl.java +++ b/server/src/org/apache/cloudstack/outofbandmanagement/OutOfBandManagementServiceImpl.java @@ -577,5 +577,11 @@ protected void runInContext() { LOG.error("Error trying to retrieve host out-of-band management stats", t); } } + + @Override + public Long getDelay() { + return null; + } + } } diff --git a/server/src/org/apache/cloudstack/poll/BackgroundPollManagerImpl.java b/server/src/org/apache/cloudstack/poll/BackgroundPollManagerImpl.java index c0a7f1c39572..f4a634032d4c 100644 --- a/server/src/org/apache/cloudstack/poll/BackgroundPollManagerImpl.java +++ b/server/src/org/apache/cloudstack/poll/BackgroundPollManagerImpl.java @@ -52,7 +52,11 @@ public boolean start() { } backgroundPollTaskScheduler = Executors.newScheduledThreadPool(submittedTasks.size() + 1, new NamedThreadFactory("BackgroundTaskPollManager")); for (final BackgroundPollTask task : submittedTasks) { - backgroundPollTaskScheduler.scheduleWithFixedDelay(task, getInitialDelay(), getRoundDelay(), TimeUnit.MILLISECONDS); + Long delay = task.getDelay(); + if (delay == null) { + delay = getRoundDelay(); + } + backgroundPollTaskScheduler.scheduleWithFixedDelay(task, getInitialDelay(), delay, TimeUnit.MILLISECONDS); LOG.debug("Scheduled background poll task: " + task.getClass().getName()); } isConfiguredAndStarted = true; diff --git a/server/test/com/cloud/server/ConfigurationServerImplTest.java b/server/test/com/cloud/server/ConfigurationServerImplTest.java index b64f3f72aa27..8b0af9990117 100644 --- a/server/test/com/cloud/server/ConfigurationServerImplTest.java +++ b/server/test/com/cloud/server/ConfigurationServerImplTest.java @@ -16,8 +16,17 @@ // under the License. package com.cloud.server; -import java.io.File; -import java.io.IOException; +import org.apache.cloudstack.framework.config.ConfigDepot; +import org.apache.cloudstack.framework.config.ConfigDepotAdmin; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.junit.Assert; +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.runners.MockitoJUnitRunner; import com.cloud.configuration.ConfigurationManager; import com.cloud.configuration.dao.ResourceCountDao; @@ -32,19 +41,6 @@ import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.user.dao.AccountDao; import com.cloud.utils.db.TransactionLegacy; -import org.apache.cloudstack.framework.config.ConfigDepot; -import org.apache.cloudstack.framework.config.ConfigDepotAdmin; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.FileUtils; -import org.junit.Assert; -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.runners.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class ConfigurationServerImplTest { @@ -101,41 +97,6 @@ protected boolean isOnWindows() { } }; - final static String TEST = "the quick brown fox jumped over the lazy dog"; - - @Test(expected = IOException.class) - public void testGetBase64KeystoreNoSuchFile() throws IOException { - ConfigurationServerImpl.getBase64Keystore("notexisting" + System.currentTimeMillis()); - } - - @Test(expected = IOException.class) - public void testGetBase64KeystoreTooBigFile() throws IOException { - File temp = File.createTempFile("keystore", ""); - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < 1000; i++) { - builder.append("way too long...\n"); - } - FileUtils.writeStringToFile(temp, builder.toString()); - try { - ConfigurationServerImpl.getBase64Keystore(temp.getPath()); - } finally { - temp.delete(); - } - } - - @Test - public void testGetBase64Keystore() throws IOException { - File temp = File.createTempFile("keystore", ""); - try { - FileUtils.writeStringToFile(temp, Base64.encodeBase64String(TEST.getBytes())); - final String keystore = ConfigurationServerImpl.getBase64Keystore(temp.getPath()); - // let's decode it to make sure it makes sense - Base64.decodeBase64(keystore); - } finally { - temp.delete(); - } - } - @Test public void testWindowsScript() { Assert.assertTrue(windowsImpl.isOnWindows()); diff --git a/server/test/com/cloud/vpc/MockNetworkManagerImpl.java b/server/test/com/cloud/vpc/MockNetworkManagerImpl.java index 495989d65be6..f6f818d17339 100644 --- a/server/test/com/cloud/vpc/MockNetworkManagerImpl.java +++ b/server/test/com/cloud/vpc/MockNetworkManagerImpl.java @@ -574,6 +574,11 @@ public List getNicProfiles(VirtualMachine vm) { return null; } + @Override + public Map getSystemVMAccessDetails(VirtualMachine vm) { + return null; + } + /* (non-Javadoc) * @see com.cloud.network.NetworkManager#implementNetwork(long, com.cloud.deploy.DeployDestination, com.cloud.vm.ReservationContext) */ diff --git a/server/test/org/apache/cloudstack/ca/CABackgroundTaskTest.java b/server/test/org/apache/cloudstack/ca/CABackgroundTaskTest.java new file mode 100644 index 000000000000..d2c800d82729 --- /dev/null +++ b/server/test/org/apache/cloudstack/ca/CABackgroundTaskTest.java @@ -0,0 +1,152 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca; + +import static org.apache.cloudstack.ca.CAManager.AutomaticCertRenewal; + +import java.lang.reflect.Field; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.cloudstack.utils.security.CertUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import com.cloud.host.Host; +import com.cloud.host.HostVO; +import com.cloud.host.Status; +import com.cloud.host.dao.HostDao; +import com.cloud.storage.Storage; +import com.cloud.utils.exception.CloudRuntimeException; + +@RunWith(MockitoJUnitRunner.class) +public class CABackgroundTaskTest { + + @Mock + private CAManager caManager; + @Mock + private HostDao hostDao; + + private String hostIp = "1.2.3.4"; + private HostVO host = new HostVO(1L, "some.host",Host.Type.Routing, hostIp, "255.255.255.0", null, null, null, null, null, null, null, null, null, null, + UUID.randomUUID().toString(), Status.Up, "1.0", null, null, 1L, null, 0, 0, "aa", 0, Storage.StoragePoolType.NetworkFilesystem); + + private X509Certificate expiredCertificate; + private Map certMap = new HashMap<>(); + private CAManagerImpl.CABackgroundTask task; + + @Before + public void setUp() throws Exception { + host.setManagementServerId(ManagementServerNode.getManagementServerId()); + task = new CAManagerImpl.CABackgroundTask(caManager, hostDao); + final KeyPair keypair = CertUtils.generateRandomKeyPair(1024); + expiredCertificate = CertUtils.generateV1Certificate(keypair, "CN=ca", "CN=ca", 0, + "SHA256withRSA"); + + Mockito.when(hostDao.findByIp(Mockito.anyString())).thenReturn(host); + Mockito.when(caManager.getActiveCertificatesMap()).thenReturn(certMap); + } + + @After + public void tearDown() throws Exception { + certMap.clear(); + Mockito.reset(caManager); + Mockito.reset(hostDao); + } + + private void overrideDefaultConfigValue(final ConfigKey configKey, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = ConfigKey.class.getDeclaredField(name); + f.setAccessible(true); + f.set(configKey, o); + } + + @Test + public void testNullCert() throws Exception { + certMap.put(hostIp, null); + Assert.assertTrue(certMap.size() == 1); + task.runInContext(); + Assert.assertTrue(certMap.size() == 0); + } + + @Test + public void testNullHost() throws Exception { + Mockito.when(hostDao.findByIp(Mockito.anyString())).thenReturn(null); + certMap.put(hostIp, expiredCertificate); + Assert.assertTrue(certMap.size() == 1); + task.runInContext(); + Assert.assertTrue(certMap.size() == 0); + } + + @Test + public void testAutoRenewalEnabledWithNoExceptionsOnProvisioning() throws Exception { + overrideDefaultConfigValue(AutomaticCertRenewal, "_defaultValue", "true"); + host.setManagementServerId(ManagementServerNode.getManagementServerId()); + certMap.put(hostIp, expiredCertificate); + Assert.assertTrue(certMap.size() == 1); + task.runInContext(); + Mockito.verify(caManager, Mockito.times(1)).provisionCertificate(host, false, null); + 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(Mockito.any(Host.class), Mockito.anyBoolean(), Mockito.anyString())).thenThrow(new CloudRuntimeException("some error")); + host.setManagementServerId(ManagementServerNode.getManagementServerId()); + certMap.put(hostIp, expiredCertificate); + Assert.assertTrue(certMap.size() == 1); + task.runInContext(); + Mockito.verify(caManager, Mockito.times(1)).provisionCertificate(host, false, null); + Mockito.verify(caManager, Mockito.times(1)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void testAutoRenewalDisabled() throws Exception { + overrideDefaultConfigValue(AutomaticCertRenewal, "_defaultValue", "false"); + certMap.put(hostIp, expiredCertificate); + Assert.assertTrue(certMap.size() == 1); + // First round + task.runInContext(); + Mockito.verify(caManager, Mockito.times(0)).provisionCertificate(Mockito.any(Host.class), Mockito.anyBoolean(), Mockito.anyString()); + 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), Mockito.anyBoolean(), Mockito.anyString()); + Mockito.verify(caManager, Mockito.times(0)).sendAlert(Mockito.any(Host.class), Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void testGetDelay() throws Exception { + Assert.assertTrue(task.getDelay() == CAManager.CABackgroundJobDelay.value() * 1000L); + } + +} \ No newline at end of file diff --git a/server/test/org/apache/cloudstack/ca/CAManagerImplTest.java b/server/test/org/apache/cloudstack/ca/CAManagerImplTest.java new file mode 100644 index 000000000000..87e128c772e4 --- /dev/null +++ b/server/test/org/apache/cloudstack/ca/CAManagerImplTest.java @@ -0,0 +1,119 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.ca; + +import java.lang.reflect.Field; +import java.math.BigInteger; +import java.security.cert.X509Certificate; +import java.util.Collections; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.framework.ca.CAProvider; +import org.apache.cloudstack.framework.ca.Certificate; +import org.apache.cloudstack.utils.security.CertUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import com.cloud.agent.AgentManager; +import com.cloud.agent.api.Answer; +import com.cloud.certificate.CrlVO; +import com.cloud.certificate.dao.CrlDao; +import com.cloud.host.Host; +import com.cloud.host.dao.HostDao; + +@RunWith(MockitoJUnitRunner.class) +public class CAManagerImplTest { + + @Mock + private HostDao hostDao; + @Mock + private CrlDao crlDao; + @Mock + private AgentManager agentManager; + @Mock + private CAProvider caProvider; + + private CAManagerImpl caManager; + + private void addField(final CAManagerImpl provider, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException { + Field f = CAManagerImpl.class.getDeclaredField(name); + f.setAccessible(true); + f.set(provider, o); + } + + @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"); + caManager.setCaProviders(Collections.singletonList(caProvider)); + } + + @After + public void tearDown() throws Exception { + Mockito.reset(crlDao); + Mockito.reset(agentManager); + Mockito.reset(caProvider); + } + + @Test(expected = ServerApiException.class) + public void testIssueCertificateThrowsException() throws Exception { + caManager.issueCertificate(null, null, null, 1, null); + } + + @Test + public void testIssueCertificate() throws Exception { + caManager.issueCertificate(null, Collections.singletonList("domain.example"), null, 1, null); + Mockito.verify(caProvider, Mockito.times(1)).issueCertificate(Mockito.anyList(), Mockito.anyList(), Mockito.anyInt()); + Mockito.verify(caProvider, Mockito.times(0)).issueCertificate(Mockito.anyString(), Mockito.anyList(), Mockito.anyList(), Mockito.anyInt()); + } + + @Test + public void testRevokeCertificate() throws Exception { + final CrlVO crl = new CrlVO(CertUtils.generateRandomBigInt(), "some.domain", "some-uuid"); + Mockito.when(crlDao.revokeCertificate(Mockito.any(BigInteger.class), Mockito.anyString())).thenReturn(crl); + Mockito.when(caProvider.revokeCertificate(Mockito.any(BigInteger.class), Mockito.anyString())).thenReturn(true); + Assert.assertTrue(caManager.revokeCertificate(crl.getCertSerial(), crl.getCertCn(), null)); + Mockito.verify(caProvider, Mockito.times(1)).revokeCertificate(Mockito.any(BigInteger.class), Mockito.anyString()); + } + + @Test + public void testProvisionCertificate() throws Exception { + final Host host = Mockito.mock(Host.class); + Mockito.when(host.getPrivateIpAddress()).thenReturn("1.2.3.4"); + final X509Certificate certificate = CertUtils.generateV1Certificate(CertUtils.generateRandomKeyPair(1024), "CN=ca", "CN=ca", 1, "SHA256withRSA"); + Mockito.when(caProvider.issueCertificate(Mockito.anyString(), Mockito.anyList(), Mockito.anyList(), Mockito.anyInt())).thenReturn(new Certificate(certificate, null, Collections.singletonList(certificate))); + Mockito.when(agentManager.send(Mockito.anyLong(), Mockito.any(SetupKeyStoreCommand.class))).thenReturn(new SetupKeystoreAnswer("someCsr")); + Mockito.when(agentManager.reconnect(Mockito.anyLong())).thenReturn(true); + Assert.assertTrue(caManager.provisionCertificate(host, true, null)); + Mockito.verify(agentManager, Mockito.times(2)).send(Mockito.anyLong(), Mockito.any(Answer.class)); + Mockito.verify(agentManager, Mockito.times(1)).reconnect(Mockito.anyLong()); + } +} \ No newline at end of file diff --git a/server/test/org/apache/cloudstack/poll/BackgroundPollManagerImplTest.java b/server/test/org/apache/cloudstack/poll/BackgroundPollManagerImplTest.java index 3304abaf6111..f35d49cefed6 100644 --- a/server/test/org/apache/cloudstack/poll/BackgroundPollManagerImplTest.java +++ b/server/test/org/apache/cloudstack/poll/BackgroundPollManagerImplTest.java @@ -45,6 +45,12 @@ protected void runInContext() { didIRun = true; counter++; } + + @Override + public Long getDelay() { + return null; + } + } @Before diff --git a/services/secondary-storage/controller/src/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java b/services/secondary-storage/controller/src/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java index 79c6bab1b4b9..273fdd0d1167 100644 --- a/services/secondary-storage/controller/src/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java +++ b/services/secondary-storage/controller/src/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java @@ -123,6 +123,7 @@ import com.cloud.utils.DateUtil; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; +import com.cloud.utils.StringUtils; import com.cloud.utils.component.ManagerBase; import com.cloud.utils.db.GlobalLock; import com.cloud.utils.db.QueryBuilder; @@ -1118,7 +1119,7 @@ public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, Depl StringBuilder buf = profile.getBootArgsBuilder(); buf.append(" template=domP type=secstorage"); - buf.append(" host=").append(ApiServiceConfiguration.ManagementHostIPAdr.value()); + buf.append(" host=").append(StringUtils.shuffleCSVList(ApiServiceConfiguration.ManagementHostIPAdr.value())); buf.append(" port=").append(_mgmtPort); buf.append(" name=").append(profile.getVirtualMachine().getHostName()); diff --git a/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java b/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java index 60424c1af3b6..4f3ad0774529 100644 --- a/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java +++ b/services/secondary-storage/server/src/org/apache/cloudstack/storage/resource/NfsSecondaryStorageResource.java @@ -16,6 +16,71 @@ // under the License. package org.apache.cloudstack.storage.resource; +import static com.cloud.utils.StringUtils.join; +import static com.cloud.utils.storage.S3.S3Utils.putFile; +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static org.apache.commons.lang.StringUtils.substringAfterLast; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.naming.ConfigurationException; + +import org.apache.cloudstack.framework.security.keystore.KeystoreManager; +import org.apache.cloudstack.storage.command.CopyCmdAnswer; +import org.apache.cloudstack.storage.command.CopyCommand; +import org.apache.cloudstack.storage.command.DeleteCommand; +import org.apache.cloudstack.storage.command.DownloadCommand; +import org.apache.cloudstack.storage.command.DownloadProgressCommand; +import org.apache.cloudstack.storage.command.TemplateOrVolumePostUploadCommand; +import org.apache.cloudstack.storage.command.UploadStatusAnswer; +import org.apache.cloudstack.storage.command.UploadStatusAnswer.UploadStatus; +import org.apache.cloudstack.storage.command.UploadStatusCommand; +import org.apache.cloudstack.storage.template.DownloadManager; +import org.apache.cloudstack.storage.template.DownloadManagerImpl; +import org.apache.cloudstack.storage.template.DownloadManagerImpl.ZfsPathParser; +import org.apache.cloudstack.storage.template.UploadEntity; +import org.apache.cloudstack.storage.template.UploadManager; +import org.apache.cloudstack.storage.template.UploadManagerImpl; +import org.apache.cloudstack.storage.to.SnapshotObjectTO; +import org.apache.cloudstack.storage.to.TemplateObjectTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; +import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.log4j.Logger; +import org.joda.time.DateTime; +import org.joda.time.format.ISODateTimeFormat; + import com.amazonaws.services.s3.model.S3ObjectSummary; import com.cloud.agent.api.Answer; import com.cloud.agent.api.CheckHealthAnswer; @@ -83,6 +148,7 @@ import com.cloud.vm.SecondaryStorageVm; import com.google.gson.Gson; import com.google.gson.GsonBuilder; + import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; @@ -96,69 +162,6 @@ import io.netty.handler.codec.http.HttpResponseEncoder; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; -import org.apache.cloudstack.framework.security.keystore.KeystoreManager; -import org.apache.cloudstack.storage.command.CopyCmdAnswer; -import org.apache.cloudstack.storage.command.CopyCommand; -import org.apache.cloudstack.storage.command.DeleteCommand; -import org.apache.cloudstack.storage.command.DownloadCommand; -import org.apache.cloudstack.storage.command.DownloadProgressCommand; -import org.apache.cloudstack.storage.command.TemplateOrVolumePostUploadCommand; -import org.apache.cloudstack.storage.command.UploadStatusAnswer; -import org.apache.cloudstack.storage.command.UploadStatusAnswer.UploadStatus; -import org.apache.cloudstack.storage.command.UploadStatusCommand; -import org.apache.cloudstack.storage.template.DownloadManager; -import org.apache.cloudstack.storage.template.DownloadManagerImpl; -import org.apache.cloudstack.storage.template.DownloadManagerImpl.ZfsPathParser; -import org.apache.cloudstack.storage.template.UploadEntity; -import org.apache.cloudstack.storage.template.UploadManager; -import org.apache.cloudstack.storage.template.UploadManagerImpl; -import org.apache.cloudstack.storage.to.SnapshotObjectTO; -import org.apache.cloudstack.storage.to.TemplateObjectTO; -import org.apache.cloudstack.storage.to.VolumeObjectTO; -import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.log4j.Logger; -import org.joda.time.DateTime; -import org.joda.time.format.ISODateTimeFormat; - -import javax.naming.ConfigurationException; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.math.BigInteger; -import java.net.InetAddress; -import java.net.URI; -import java.net.UnknownHostException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import static com.cloud.utils.StringUtils.join; -import static com.cloud.utils.storage.S3.S3Utils.putFile; -import static java.lang.String.format; -import static java.util.Arrays.asList; -import static org.apache.commons.lang.StringUtils.substringAfterLast; public class NfsSecondaryStorageResource extends ServerResourceBase implements SecondaryStorageResource { @@ -2231,9 +2234,10 @@ public boolean configure(String name, Map params) throws Configu if (_inSystemVM) { _localgw = (String)params.get("localgw"); if (_localgw != null) { // can only happen inside service vm - String mgmtHost = (String)params.get("host"); - addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost); - + String mgmtHosts = (String)params.get("host"); + for (final String mgmtHost : mgmtHosts.split(",")) { + addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost); + } String internalDns1 = (String)params.get("internaldns1"); if (internalDns1 == null) { s_logger.warn("No DNS entry found during configuration of NfsSecondaryStorage"); diff --git a/setup/db/db/schema-41000to41100.sql b/setup/db/db/schema-41000to41100.sql index 911def8f2c3b..eacddc15c4dc 100644 --- a/setup/db/db/schema-41000to41100.sql +++ b/setup/db/db/schema-41000to41100.sql @@ -123,3 +123,18 @@ CREATE VIEW `template_view` AS OR (`resource_tags`.`resource_type` = 'ISO'))))); UPDATE `cloud`.`configuration` SET value = '600', default_value = '600' WHERE category = 'Advanced' AND name = 'router.aggregation.command.each.timeout'; + +-- CA framework changes +DELETE from `cloud`.`configuration` where name='ssl.keystore'; + +-- Certificate Revocation List +CREATE TABLE IF NOT EXISTS `cloud`.`crl` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `serial` varchar(255) UNIQUE NOT NULL COMMENT 'certificate\'s serial number as hex string', + `cn` varchar(255) COMMENT 'certificate\'s common name', + `revoker_uuid` varchar(40) COMMENT 'revoker user account uuid', + `revoked` datetime COMMENT 'date of revocation', + PRIMARY KEY (`id`), + KEY (`serial`), + UNIQUE KEY (`serial`, `cn`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/setup/db/server-setup.sql b/setup/db/server-setup.sql index df2c9248552a..1c4635c72cad 100644 --- a/setup/db/server-setup.sql +++ b/setup/db/server-setup.sql @@ -27,3 +27,6 @@ INSERT INTO `cloud`.`configuration` (category, instance, component, name, value, -- Enable dynamic RBAC by default for fresh deployments INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) VALUES ('Advanced', 'DEFAULT', 'RoleService', 'dynamic.apichecker.enabled', 'true'); + +-- Enable RootCA auth strictness for fresh deployments +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) VALUES ('Advanced', 'DEFAULT', 'RootCAProvider', 'ca.plugin.root.auth.strictness', 'true'); diff --git a/setup/db/server-setup.xml b/setup/db/server-setup.xml index 178f29aea352..955a3a518065 100755 --- a/setup/db/server-setup.xml +++ b/setup/db/server-setup.xml @@ -246,6 +246,13 @@ under the License. true + + ca.plugin.root.auth.strictness + true + + h17{-Vz#T*S_cfW$bSW$IYrZ8IPDPnxooL(fYT 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⪚o2Tdo#GUv5cDdtahEwPz(5<9|VbS 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%0AWs=Z@iOeYR% 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 z

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 sc5x8VLH@6QfdA_O|NrCJY#sym+IB65Bq + diff --git a/ui/scripts/ui-custom/ca.js b/ui/scripts/ui-custom/ca.js new file mode 100644 index 000000000000..c52982923714 --- /dev/null +++ b/ui/scripts/ui-custom/ca.js @@ -0,0 +1,53 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +(function($, cloudStack) { + $(window).bind('cloudStack.ready', function() { + var caCert = ""; + var downloadCaCert = function() { + var blob = new Blob([caCert], {type: 'application/x-x509-ca-cert'}); + var filename = "cloud-ca.pem"; + if(window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(blob, filename); + } else{ + var elem = window.document.createElement('a'); + elem.href = window.URL.createObjectURL(blob); + elem.download = filename; + document.body.appendChild(elem) + elem.click(); + document.body.removeChild(elem); + } + }; + + $.ajax({ + url: createURL('listCaCertificate'), + success: function(json) { + caCert = json.listcacertificateresponse.cacertificates.certificate; + if (caCert) { + var $caCertDownloadButton = $('

').addClass('cacert-download'); + $caCertDownloadButton.append($('').addClass('icon').html(' ').attr('title', 'Download CA Certificate')); + $caCertDownloadButton.click(function() { + downloadCaCert(); + }); + $('#header .controls .view-switcher:last').after($caCertDownloadButton); + } + }, + error: function(data) { + } + }); + }); +}(jQuery, cloudStack)); diff --git a/utils/pom.xml b/utils/pom.xml index 013d6831851e..52e5dd5fd4f8 100755 --- a/utils/pom.xml +++ b/utils/pom.xml @@ -35,6 +35,11 @@ cloud-framework-managed-context ${project.version} + + org.apache.cloudstack + cloud-framework-ca + ${project.version} + org.springframework spring-context @@ -71,6 +76,10 @@ org.bouncycastle bcprov-jdk15on + + org.bouncycastle + bcpkix-jdk15on + com.jcraft jsch diff --git a/utils/src/main/java/com/cloud/utils/StringUtils.java b/utils/src/main/java/com/cloud/utils/StringUtils.java index 9554e87c35ee..6ada2ad60bd1 100644 --- a/utils/src/main/java/com/cloud/utils/StringUtils.java +++ b/utils/src/main/java/com/cloud/utils/StringUtils.java @@ -21,10 +21,12 @@ import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Random; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -320,4 +322,10 @@ private static List> partitionList(final List originalList, final } return listOfChunks; } + + public static String shuffleCSVList(final String csvList) { + List list = csvTagsToList(csvList); + Collections.shuffle(list, new Random(System.nanoTime())); + return join(list, ","); + } } diff --git a/utils/src/main/java/com/cloud/utils/exception/TaskExecutionException.java b/utils/src/main/java/com/cloud/utils/exception/TaskExecutionException.java index be639baeaeb5..635874e9c816 100644 --- a/utils/src/main/java/com/cloud/utils/exception/TaskExecutionException.java +++ b/utils/src/main/java/com/cloud/utils/exception/TaskExecutionException.java @@ -25,7 +25,7 @@ * Used by the Task class to wrap-up its exceptions. */ public class TaskExecutionException extends Exception { - private static final long serialVersionUID = SerialVersionUID.NioConnectionException; + private static final long serialVersionUID = SerialVersionUID.TaskExecutionException; protected int csErrorCode; 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 02ffaab4e02a..e8f36c69944d 100644 --- a/utils/src/main/java/com/cloud/utils/nio/Link.java +++ b/utils/src/main/java/com/cloud/utils/nio/Link.java @@ -19,20 +19,6 @@ package com.cloud.utils.nio; -import com.cloud.utils.PropertiesUtil; -import com.cloud.utils.db.DbProperties; -import org.apache.cloudstack.utils.security.SSLUtils; -import org.apache.log4j.Logger; - -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLEngineResult; -import javax.net.ssl.SSLEngineResult.HandshakeStatus; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -44,8 +30,27 @@ import java.nio.channels.SocketChannel; import java.security.GeneralSecurityException; import java.security.KeyStore; +import java.security.SecureRandom; import java.util.concurrent.ConcurrentLinkedQueue; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.apache.cloudstack.framework.ca.CAService; +import org.apache.cloudstack.utils.security.KeyStoreUtils; +import org.apache.cloudstack.utils.security.SSLUtils; +import org.apache.log4j.Logger; + +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.db.DbProperties; + /** */ public class Link { @@ -62,7 +67,6 @@ public class Link { private boolean _gotFollowingPacket; private SSLEngine _sslEngine; - public static final String keystoreFile = "/cloudmanagementserver.keystore"; public Link(InetSocketAddress addr, NioConnection connection) { _addr = addr; @@ -97,49 +101,6 @@ public void setSSLEngine(SSLEngine sslEngine) { _sslEngine = sslEngine; } - /** - * No user, so comment it out. - * - * Static methods for reading from a channel in case - * you need to add a client that doesn't require nio. - * @param ch channel to read from. - * @param bytebuffer to use. - * @return bytes read - * @throws IOException if not read to completion. - public static byte[] read(SocketChannel ch, ByteBuffer buff) throws IOException { - synchronized(buff) { - buff.clear(); - buff.limit(4); - - while (buff.hasRemaining()) { - if (ch.read(buff) == -1) { - throw new IOException("Connection closed with -1 on reading size."); - } - } - - buff.flip(); - - int length = buff.getInt(); - ByteArrayOutputStream output = new ByteArrayOutputStream(length); - WritableByteChannel outCh = Channels.newChannel(output); - - int count = 0; - while (count < length) { - buff.clear(); - int read = ch.read(buff); - if (read < 0) { - throw new IOException("Connection closed with -1 on reading data."); - } - count += read; - buff.flip(); - outCh.write(buff); - } - - return output.toByteArray(); - } - } - */ - private static void doWrite(SocketChannel ch, ByteBuffer[] buffers, SSLEngine sslEngine) throws IOException { SSLSession sslSession = sslEngine.getSession(); ByteBuffer pkgBuf = ByteBuffer.allocate(sslSession.getPacketBufferSize() + 40); @@ -404,44 +365,78 @@ public synchronized void schedule(Task task) throws ClosedChannelException { _connection.scheduleTask(task); } - public static SSLContext initSSLContext(boolean isClient) throws GeneralSecurityException, IOException { - InputStream stream; - SSLContext sslContext = null; - KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); - TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); - KeyStore ks = KeyStore.getInstance("JKS"); - TrustManager[] tms; + public static SSLEngine initServerSSLEngine(final CAService caService, final String clientAddress) throws GeneralSecurityException, IOException { + final SSLContext sslContext = SSLUtils.getSSLContext(); + if (caService != null) { + return caService.createSSLEngine(sslContext, clientAddress); + } + s_logger.error("CA service is not configured, by-passing CA manager to create SSL engine"); + char[] passphrase = KeyStoreUtils.defaultKeystorePassphrase; + final KeyStore ks = loadKeyStore(NioConnection.class.getResourceAsStream("/cloud.keystore"), passphrase); + final KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + final TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + kmf.init(ks, passphrase); + tmf.init(ks); + sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom()); + return sslContext.createSSLEngine(); + } + + public static KeyStore loadKeyStore(final InputStream stream, final char[] passphrase) throws GeneralSecurityException, IOException { + final KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(stream, passphrase); + return ks; + } - File confFile = PropertiesUtil.findConfigFile("db.properties"); - if (null != confFile && !isClient) { - final String pass = DbProperties.getDbProperties().getProperty("db.cloud.keyStorePassphrase"); - char[] passphrase = "vmops.com".toCharArray(); + public static SSLContext initClientSSLContext() throws GeneralSecurityException, IOException { + final SSLContext sslContext = SSLUtils.getSSLContext(); + + char[] passphrase = KeyStoreUtils.defaultKeystorePassphrase; + File confFile = PropertiesUtil.findConfigFile("agent.properties"); + if (confFile != null) { + s_logger.info("Conf file found: " + confFile.getAbsolutePath()); + final String pass = PropertiesUtil.loadFromFile(confFile).getProperty(KeyStoreUtils.passphrasePropertyName); if (pass != null) { passphrase = pass.toCharArray(); } - String confPath = confFile.getParent(); - String keystorePath = confPath + keystoreFile; + } else { + confFile = PropertiesUtil.findConfigFile("db.properties"); + if (confFile != null) { + final String pass = DbProperties.getDbProperties().getProperty("db.cloud.keyStorePassphrase"); + if (pass != null) { + passphrase = pass.toCharArray(); + } + } + } + + InputStream stream = null; + if (confFile != null) { + final String keystorePath = confFile.getParent() + "/" + KeyStoreUtils.defaultKeystoreFile; if (new File(keystorePath).exists()) { stream = new FileInputStream(keystorePath); - } else { - s_logger.warn("SSL: Fail to find the generated keystore. Loading fail-safe one to continue."); - stream = NioConnection.class.getResourceAsStream("/cloud.keystore"); - passphrase = "vmops.com".toCharArray(); } - ks.load(stream, passphrase); - stream.close(); - kmf.init(ks, passphrase); - tmf.init(ks); + } + + final KeyStore ks = loadKeyStore(stream, passphrase); + final TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(ks); + TrustManager[] tms; + if (stream != null) { + // This enforces a two-way SSL authentication tms = tmf.getTrustManagers(); } else { - ks.load(null, null); - kmf.init(ks, null); - tms = new TrustManager[1]; - tms[0] = new TrustAllManager(); + // This enforces a one-way SSL authentication + tms = new TrustManager[]{new TrustAllManager()}; + s_logger.warn("Failed to load keystore, using trust all manager"); } - sslContext = SSLUtils.getSSLContext(); - sslContext.init(kmf.getKeyManagers(), tms, null); + if (stream != null) { + stream.close(); + } + + final KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ks, passphrase); + sslContext.init(kmf.getKeyManagers(), tms, new SecureRandom()); + if (s_logger.isTraceEnabled()) { s_logger.trace("SSL: SSLcontext has been initialized"); } @@ -498,8 +493,9 @@ private static boolean doHandshakeUnwrap(final SocketChannel socketChannel, fina try { result = sslEngine.unwrap(peerNetData, peerAppData); peerNetData.compact(); - } catch (SSLException sslException) { - s_logger.error("SSL error occurred while processing unwrap data: " + sslException.getMessage()); + } catch (final SSLException sslException) { + s_logger.error(String.format("SSL error caught during unwrap data: %s, for local address=%s, remote address=%s. The client may have invalid ca-certificates.", + sslException.getMessage(), socketChannel.getLocalAddress(), socketChannel.getRemoteAddress())); sslEngine.closeOutbound(); return true; } @@ -539,8 +535,9 @@ private static boolean doHandshakeWrap(final SocketChannel socketChannel, final SSLEngineResult result = null; try { result = sslEngine.wrap(myAppData, myNetData); - } catch (SSLException sslException) { - s_logger.error("SSL error occurred while processing wrap data: " + sslException.getMessage()); + } catch (final SSLException sslException) { + s_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 true; } diff --git a/utils/src/main/java/com/cloud/utils/nio/NioClient.java b/utils/src/main/java/com/cloud/utils/nio/NioClient.java index dc4f670de129..1c29b0c1a2d5 100644 --- a/utils/src/main/java/com/cloud/utils/nio/NioClient.java +++ b/utils/src/main/java/com/cloud/utils/nio/NioClient.java @@ -56,7 +56,7 @@ protected void init() throws IOException { _clientConnection.connect(peerAddr); _clientConnection.configureBlocking(false); - final SSLContext sslContext = Link.initSSLContext(true); + final SSLContext sslContext = Link.initClientSSLContext(); SSLEngine sslEngine = sslContext.createSSLEngine(_host, _port); sslEngine.setUseClientMode(true); sslEngine.setEnabledProtocols(SSLUtils.getSupportedProtocols(sslEngine.getEnabledProtocols())); diff --git a/utils/src/main/java/com/cloud/utils/nio/NioConnection.java b/utils/src/main/java/com/cloud/utils/nio/NioConnection.java index ce032462ec23..30000cf618b6 100644 --- a/utils/src/main/java/com/cloud/utils/nio/NioConnection.java +++ b/utils/src/main/java/com/cloud/utils/nio/NioConnection.java @@ -19,13 +19,8 @@ package com.cloud.utils.nio; -import com.cloud.utils.concurrency.NamedThreadFactory; -import com.cloud.utils.exception.NioConnectionException; -import org.apache.cloudstack.utils.security.SSLUtils; -import org.apache.log4j.Logger; +import static com.cloud.utils.AutoCloseableUtil.closeAutoCloseable; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; import java.io.IOException; import java.net.ConnectException; import java.net.InetSocketAddress; @@ -49,7 +44,14 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import static com.cloud.utils.AutoCloseableUtil.closeAutoCloseable; +import javax.net.ssl.SSLEngine; + +import org.apache.cloudstack.framework.ca.CAService; +import org.apache.cloudstack.utils.security.SSLUtils; +import org.apache.log4j.Logger; + +import com.cloud.utils.concurrency.NamedThreadFactory; +import com.cloud.utils.exception.NioConnectionException; /** * NioConnection abstracts the NIO socket operations. The Java implementation @@ -70,6 +72,7 @@ public abstract class NioConnection implements Callable { protected String _name; protected ExecutorService _executor; protected ExecutorService _sslHandshakeExecutor; + protected CAService caService; public NioConnection(final String name, final int port, final int workers, final HandlerFactory factory) { _name = name; @@ -81,6 +84,10 @@ public NioConnection(final String name, final int port, final int workers, final _sslHandshakeExecutor = Executors.newCachedThreadPool(new NamedThreadFactory(name + "-SSLHandshakeHandler")); } + public void setCAService(final CAService caService) { + this.caService = caService; + } + public void start() throws NioConnectionException { _todos = new ArrayList(); @@ -124,7 +131,7 @@ public boolean isStartup() { public Boolean call() throws NioConnectionException { while (_isRunning) { try { - _selector.select(100); + _selector.select(50); // Someone is ready for I/O, get the ready keys final Set readyKeys = _selector.selectedKeys(); @@ -196,10 +203,8 @@ protected void accept(final SelectionKey key) throws IOException { final SSLEngine sslEngine; try { - final SSLContext sslContext = Link.initSSLContext(false); - sslEngine = sslContext.createSSLEngine(); + sslEngine = Link.initServerSSLEngine(caService, socketChannel.getRemoteAddress().toString()); sslEngine.setUseClientMode(false); - sslEngine.setNeedClientAuth(false); sslEngine.setEnabledProtocols(SSLUtils.getSupportedProtocols(sslEngine.getEnabledProtocols())); final NioConnection nioConnection = this; _sslHandshakeExecutor.submit(new Runnable() { diff --git a/utils/src/main/java/com/cloud/utils/nio/NioServer.java b/utils/src/main/java/com/cloud/utils/nio/NioServer.java index b655f1838bbc..ff54165841e9 100644 --- a/utils/src/main/java/com/cloud/utils/nio/NioServer.java +++ b/utils/src/main/java/com/cloud/utils/nio/NioServer.java @@ -27,6 +27,7 @@ import java.nio.channels.spi.SelectorProvider; import java.util.WeakHashMap; +import org.apache.cloudstack.framework.ca.CAService; import org.apache.log4j.Logger; public class NioServer extends NioConnection { @@ -37,8 +38,9 @@ public class NioServer extends NioConnection { protected WeakHashMap _links; - public NioServer(final String name, final int port, final int workers, final HandlerFactory factory) { + public NioServer(final String name, final int port, final int workers, final HandlerFactory factory, final CAService caService) { super(name, port, workers, factory); + setCAService(caService); _localAddr = null; _links = new WeakHashMap(1024); } diff --git a/utils/src/main/java/com/cloud/utils/script/Script.java b/utils/src/main/java/com/cloud/utils/script/Script.java index 760847514175..01f18bda2d2e 100644 --- a/utils/src/main/java/com/cloud/utils/script/Script.java +++ b/utils/src/main/java/com/cloud/utils/script/Script.java @@ -37,6 +37,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.apache.cloudstack.utils.security.KeyStoreUtils; import org.apache.commons.io.IOUtils; import org.apache.log4j.Logger; import org.joda.time.Duration; @@ -202,7 +203,7 @@ public String execute(OutputInterpreter interpreter) { String[] command = _command.toArray(new String[_command.size()]); if (_logger.isDebugEnabled()) { - _logger.debug("Executing: " + buildCommandLine(command)); + _logger.debug("Executing: " + buildCommandLine(command).split(KeyStoreUtils.defaultKeystoreFile)[0]); } try { diff --git a/utils/src/main/java/com/cloud/utils/ssh/SSHCmdHelper.java b/utils/src/main/java/com/cloud/utils/ssh/SSHCmdHelper.java index 61d01c4ebd94..10407b65642e 100644 --- a/utils/src/main/java/com/cloud/utils/ssh/SSHCmdHelper.java +++ b/utils/src/main/java/com/cloud/utils/ssh/SSHCmdHelper.java @@ -22,8 +22,10 @@ import java.io.IOException; import java.io.InputStream; +import org.apache.cloudstack.utils.security.KeyStoreUtils; import org.apache.log4j.Logger; +import com.google.common.base.Strings; import com.trilead.ssh2.ChannelCondition; import com.trilead.ssh2.Session; @@ -32,6 +34,44 @@ public class SSHCmdHelper { private static final int DEFAULT_CONNECT_TIMEOUT = 180000; private static final int DEFAULT_KEX_TIMEOUT = 60000; + public static class SSHCmdResult { + private int returnCode = -1; + private String stdOut; + private String stdErr; + + public SSHCmdResult(final int returnCode, final String stdOut, final String stdErr) { + this.returnCode = returnCode; + this.stdOut = stdOut; + this.stdErr = stdErr; + } + + @Override + public String toString() { + return String.format("SSH cmd result: return code=%d, stdout=%s, stderr=%s", + getReturnCode(), getStdOut().split("-----BEGIN")[0], getStdErr()); + } + + public boolean isSuccess() { + return returnCode == 0; + } + + public int getReturnCode() { + return returnCode; + } + + public void setReturnCode(int returnCode) { + this.returnCode = returnCode; + } + + public String getStdOut() { + return stdOut; + } + + public String getStdErr() { + return stdErr; + } + } + public static com.trilead.ssh2.Connection acquireAuthorizedConnection(String ip, String username, String password) { return acquireAuthorizedConnection(ip, 22, username, password); } @@ -65,36 +105,41 @@ public static void releaseSshConnection(com.trilead.ssh2.Connection sshConnectio public static boolean sshExecuteCmd(com.trilead.ssh2.Connection sshConnection, String cmd, int nTimes) { for (int i = 0; i < nTimes; i++) { try { - if (sshExecuteCmdOneShot(sshConnection, cmd)) + final SSHCmdResult result = sshExecuteCmdOneShot(sshConnection, cmd); + if (result.isSuccess()) { return true; - } catch (SshException e) { + } + } catch (SshException ignored) { continue; } } return false; } - public static int sshExecuteCmdWithExitCode(com.trilead.ssh2.Connection sshConnection, String cmd) { - return sshExecuteCmdWithExitCode(sshConnection, cmd, 3); - } - - public static int sshExecuteCmdWithExitCode(com.trilead.ssh2.Connection sshConnection, String cmd, int nTimes) { + public static SSHCmdResult sshExecuteCmdWithResult(com.trilead.ssh2.Connection sshConnection, String cmd, int nTimes) { for (int i = 0; i < nTimes; i++) { try { - return sshExecuteCmdOneShotWithExitCode(sshConnection, cmd); - } catch (SshException e) { + final SSHCmdResult result = sshExecuteCmdOneShot(sshConnection, cmd); + if (result.isSuccess()) { + return result; + } + } catch (SshException ignored) { continue; } } - return -1; + return new SSHCmdResult(-1, null, null); } public static boolean sshExecuteCmd(com.trilead.ssh2.Connection sshConnection, String cmd) { return sshExecuteCmd(sshConnection, cmd, 3); } - public static int sshExecuteCmdOneShotWithExitCode(com.trilead.ssh2.Connection sshConnection, String cmd) throws SshException { - s_logger.debug("Executing cmd: " + cmd); + public static SSHCmdResult sshExecuteCmdWithResult(com.trilead.ssh2.Connection sshConnection, String cmd) { + return sshExecuteCmdWithResult(sshConnection, cmd, 3); + } + + public static SSHCmdResult sshExecuteCmdOneShot(com.trilead.ssh2.Connection sshConnection, String cmd) throws SshException { + s_logger.debug("Executing cmd: " + cmd.split(KeyStoreUtils.defaultKeystoreFile)[0]); Session sshSession = null; try { sshSession = sshConnection.openSession(); @@ -112,7 +157,8 @@ public static int sshExecuteCmdOneShotWithExitCode(com.trilead.ssh2.Connection s InputStream stderr = sshSession.getStderr(); byte[] buffer = new byte[8192]; - StringBuffer sbResult = new StringBuffer(); + StringBuffer sbStdoutResult = new StringBuffer(); + StringBuffer sbStdErrResult = new StringBuffer(); int currentReadBytes = 0; while (true) { @@ -145,27 +191,30 @@ public static int sshExecuteCmdOneShotWithExitCode(com.trilead.ssh2.Connection s while (stdout.available() > 0) { currentReadBytes = stdout.read(buffer); - sbResult.append(new String(buffer, 0, currentReadBytes)); + sbStdoutResult.append(new String(buffer, 0, currentReadBytes)); } while (stderr.available() > 0) { currentReadBytes = stderr.read(buffer); - sbResult.append(new String(buffer, 0, currentReadBytes)); + sbStdErrResult.append(new String(buffer, 0, currentReadBytes)); } } - String result = sbResult.toString(); - if (result != null && !result.isEmpty()) - s_logger.debug(cmd + " output:" + result); + final SSHCmdResult result = new SSHCmdResult(-1, sbStdoutResult.toString(), sbStdErrResult.toString()); + if (!Strings.isNullOrEmpty(result.getStdOut()) || !Strings.isNullOrEmpty(result.getStdErr())) { + s_logger.debug("SSH command: " + cmd.split(KeyStoreUtils.defaultKeystoreFile)[0] + "\nSSH command output:" + result.getStdOut().split("-----BEGIN")[0] + "\n" + result.getStdErr()); + } + // exit status delivery might get delayed for(int i = 0 ; i<10 ; i++ ) { Integer status = sshSession.getExitStatus(); if( status != null ) { - return status; + result.setReturnCode(status); + return result; } Thread.sleep(100); } - return -1; + return result; } catch (Exception e) { s_logger.debug("Ssh executed failed", e); throw new SshException("Ssh executed failed " + e.getMessage()); @@ -174,8 +223,4 @@ public static int sshExecuteCmdOneShotWithExitCode(com.trilead.ssh2.Connection s sshSession.close(); } } - - public static boolean sshExecuteCmdOneShot(com.trilead.ssh2.Connection sshConnection, String cmd) throws SshException { - return sshExecuteCmdOneShotWithExitCode(sshConnection, cmd) == 0; - } } 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 new file mode 100644 index 000000000000..c2ef9edb8c40 --- /dev/null +++ b/utils/src/main/java/org/apache/cloudstack/utils/security/CertUtils.java @@ -0,0 +1,240 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.utils.security; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.List; + +import javax.security.auth.x500.X500Principal; + +import org.apache.log4j.Logger; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v1CertificateBuilder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.cert.jcajce.JcaX509v1CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; +import org.bouncycastle.util.io.pem.PemWriter; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import com.google.common.base.Strings; + +//import org.bouncycastle.x509.extension.SubjectKeyIdentifierStructure; + +public class CertUtils { + + private static final Logger LOG = Logger.getLogger(CertUtils.class); + + public static KeyPair generateRandomKeyPair(final int keySize) throws NoSuchProviderException, NoSuchAlgorithmException { + Security.addProvider(new BouncyCastleProvider()); + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); + keyPairGenerator.initialize(keySize, new SecureRandom()); + return keyPairGenerator.generateKeyPair(); + } + + public static KeyFactory getKeyFactory() { + KeyFactory keyFactory = null; + try { + Security.addProvider(new BouncyCastleProvider()); + keyFactory = KeyFactory.getInstance("RSA", "BC"); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + LOG.error("Unable to create KeyFactory:" + e.getMessage()); + } + 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 String x509CertificateToPem(final X509Certificate cert) throws IOException { + final StringWriter sw = new StringWriter(); + try (final JcaPEMWriter pw = new JcaPEMWriter(sw)) { + pw.writeObject(cert); + pw.flush(); + } + return sw.toString(); + } + + public static String x509CertificatesToPem(final List certificates) throws IOException { + if (certificates == null) { + return ""; + } + final StringBuilder buffer = new StringBuilder(); + for (final X509Certificate certificate: certificates) { + buffer.append(CertUtils.x509CertificateToPem(certificate)); + } + return buffer.toString(); + } + + public static PrivateKey pemToPrivateKey(final String pem) throws InvalidKeySpecException, IOException { + final PemReader pr = new PemReader(new StringReader(pem)); + final PemObject pemObject = pr.readPemObject(); + final KeyFactory keyFactory = getKeyFactory(); + return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(pemObject.getContent())); + } + + public static String privateKeyToPem(final PrivateKey key) throws IOException { + final PemObject pemObject = new PemObject("RSA PRIVATE KEY", key.getEncoded()); + final StringWriter sw = new StringWriter(); + try (final PemWriter pw = new PemWriter(sw)) { + pw.writeObject(pemObject); + } + return sw.toString(); + } + + public static PublicKey pemToPublicKey(final String pem) throws InvalidKeySpecException, IOException { + final PemReader pr = new PemReader(new StringReader(pem)); + final PemObject pemObject = pr.readPemObject(); + final KeyFactory keyFactory = getKeyFactory(); + return keyFactory.generatePublic(new X509EncodedKeySpec(pemObject.getContent())); + } + + public static String publicKeyToPem(final PublicKey key) throws IOException { + final PemObject pemObject = new PemObject("PUBLIC KEY", key.getEncoded()); + final StringWriter sw = new StringWriter(); + try (final PemWriter pw = new PemWriter(sw)) { + pw.writeObject(pemObject); + } + return sw.toString(); + } + + public static BigInteger generateRandomBigInt() { + return new BigInteger(64, new SecureRandom()); + } + + public static X509Certificate generateV1Certificate(final KeyPair keyPair, + final String subject, + final String issuer, + final int validityYears, + final String signatureAlgorithm) throws CertificateException, NoSuchAlgorithmException, NoSuchProviderException, SignatureException, InvalidKeyException, OperatorCreationException { + final DateTime now = DateTime.now(DateTimeZone.UTC); + final X509v1CertificateBuilder certBuilder = new JcaX509v1CertificateBuilder( + new X500Name(issuer), + generateRandomBigInt(), + now.minusDays(1).toDate(), + now.plusYears(validityYears).toDate(), + new X500Name(subject), + keyPair.getPublic()); + final ContentSigner signer = new JcaContentSignerBuilder(signatureAlgorithm).setProvider("BC").build(keyPair.getPrivate()); + final X509CertificateHolder certHolder = certBuilder.build(signer); + return new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder); + } + + public static X509Certificate generateV3Certificate(final X509Certificate caCert, + final PrivateKey caPrivateKey, + final PublicKey clientPublicKey, + final String subject, + final String signatureAlgorithm, + final int validityDays, + final List dnsNames, + final List publicIPAddresses) throws IOException, NoSuchAlgorithmException, CertificateException, NoSuchProviderException, InvalidKeyException, SignatureException, OperatorCreationException { + + final DateTime now = DateTime.now(DateTimeZone.UTC); + final BigInteger serial = generateRandomBigInt(); + final X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + caCert, + serial, + now.minusHours(12).toDate(), + now.plusDays(validityDays).toDate(), + new X500Principal(subject), + clientPublicKey); + + final JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils(); + + certBuilder.addExtension( + Extension.subjectKeyIdentifier, + false, + extUtils.createSubjectKeyIdentifier(clientPublicKey)); + + certBuilder.addExtension( + Extension.authorityKeyIdentifier, + false, + extUtils.createAuthorityKeyIdentifier(caCert)); + + final List subjectAlternativeNames = new ArrayList(); + if (publicIPAddresses != null) { + for (final String publicIPAddress: publicIPAddresses) { + if (Strings.isNullOrEmpty(publicIPAddress)) { + continue; + } + subjectAlternativeNames.add(new GeneralName(GeneralName.iPAddress, publicIPAddress)); + } + } + if (dnsNames != null) { + for (final String dnsName : dnsNames) { + if (Strings.isNullOrEmpty(dnsName)) { + continue; + } + subjectAlternativeNames.add(new GeneralName(GeneralName.dNSName, dnsName)); + } + } + if (subjectAlternativeNames.size() > 0) { + final GeneralNames subjectAltNames = GeneralNames.getInstance(new DERSequence(subjectAlternativeNames.toArray(new ASN1Encodable[] {}))); + certBuilder.addExtension( + Extension.subjectAlternativeName, + false, + subjectAltNames); + } + + final ContentSigner signer = new JcaContentSignerBuilder(signatureAlgorithm).setProvider("BC").build(caPrivateKey); + final X509CertificateHolder certHolder = certBuilder.build(signer); + final X509Certificate cert = new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder); + cert.verify(caCert.getPublicKey()); + return cert; + } +} 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 new file mode 100644 index 000000000000..e02d3b09d8f3 --- /dev/null +++ b/utils/src/main/java/org/apache/cloudstack/utils/security/KeyStoreUtils.java @@ -0,0 +1,70 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.utils.security; + +import java.io.File; +import java.io.IOException; + +import com.cloud.utils.script.Script; +import com.google.common.base.Strings; + +public class KeyStoreUtils { + + public static String defaultTmpKeyStoreFile = "/tmp/tmp.jks"; + public static String defaultKeystoreFile = "cloud.jks"; + public static String defaultPrivateKeyFile = "cloud.key"; + public static String defaultCsrFile = "cloud.csr"; + public static String defaultCertFile = "cloud.crt"; + public static String defaultCaCertFile = "cloud.ca.crt"; + public static char[] defaultKeystorePassphrase = "vmops.com".toCharArray(); + + public static String certNewlineEncoder = "^"; + public static String certSpaceEncoder = "~"; + + public static String keyStoreSetupScript = "keystore-setup"; + public static String keyStoreImportScript = "keystore-cert-import"; + public static String passphrasePropertyName = "keystore.passphrase"; + + public static String sshMode = "ssh"; + public static String agentMode = "agent"; + + public static void copyKeystore(final String keystorePath, final String tmpKeystorePath) throws IOException { + if (Strings.isNullOrEmpty(keystorePath) || Strings.isNullOrEmpty(tmpKeystorePath)) { + throw new IOException("Invalid keystore path provided"); + } + try { + final Script script = new Script(true, "cp", 5000, null); + script.add("-f"); + script.add(tmpKeystorePath); + script.add(keystorePath); + final String result = script.execute(); + if (result != null) { + throw new IOException("Failed to execute cp to copy keystore file to mgmt server conf location"); + } + } catch (final Exception e) { + throw new IOException("Failed to create keystore file: " + keystorePath, e); + } + try { + new File(tmpKeystorePath).delete(); + } catch (Exception ignored) { + } + } + +} diff --git a/utils/src/test/java/com/cloud/utils/StringUtilsTest.java b/utils/src/test/java/com/cloud/utils/StringUtilsTest.java index 3619ede84b2b..e8e62b0a75e7 100644 --- a/utils/src/test/java/com/cloud/utils/StringUtilsTest.java +++ b/utils/src/test/java/com/cloud/utils/StringUtilsTest.java @@ -20,6 +20,8 @@ package com.cloud.utils; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertNotEquals; import java.nio.charset.Charset; @@ -250,4 +252,15 @@ public void listToCsvTags() { assertEquals("a,b,c", StringUtils.listToCsvTags(Arrays.asList("a","b", "c"))); assertEquals("", StringUtils.listToCsvTags(new ArrayList())); } + + @Test + public void testShuffleCSVList() { + String input = "one,two,three,four,five,six,seven,eight,nine,ten"; + String output = StringUtils.shuffleCSVList(input); + assertFalse(input.equals(output)); + + input = "only-one"; + output = StringUtils.shuffleCSVList("only-one"); + assertTrue(input.equals(output)); + } } diff --git a/utils/src/test/java/com/cloud/utils/testcase/NioTest.java b/utils/src/test/java/com/cloud/utils/testcase/NioTest.java index 894aa1adc88d..0a9deea1a9d6 100644 --- a/utils/src/test/java/com/cloud/utils/testcase/NioTest.java +++ b/utils/src/test/java/com/cloud/utils/testcase/NioTest.java @@ -98,7 +98,7 @@ public void setUp() { testBytes = new byte[1000000]; randomGenerator.nextBytes(testBytes); - server = new NioServer("NioTestServer", 0, 1, new NioTestServer()); + server = new NioServer("NioTestServer", 0, 1, new NioTestServer(), null); try { server.start(); } catch (final NioConnectionException e) { 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 new file mode 100644 index 000000000000..406f60449f7b --- /dev/null +++ b/utils/src/test/java/org/apache/cloudstack/utils/security/CertUtilsTest.java @@ -0,0 +1,118 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package org.apache.cloudstack.utils.security; + +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; +import java.util.Arrays; +import java.util.List; + +import org.bouncycastle.asn1.x509.GeneralName; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class CertUtilsTest { + KeyPair caKeyPair; + X509Certificate caCertificate; + + @Before + public void setUp() throws Exception { + caKeyPair = CertUtils.generateRandomKeyPair(1024); + caCertificate = CertUtils.generateV1Certificate(caKeyPair, "CN=test", "CN=test", 1, "SHA256WithRSAEncryption"); + } + + @Test + public void testGenerateRandomKeyPair() throws Exception { + final int size = 2048; + final KeyPair kp = CertUtils.generateRandomKeyPair(size); + Assert.assertEquals(((RSAPublicKey)kp.getPublic()).getModulus().bitLength(), size); + } + + @Test + public void testCertificateConversionMethods() throws Exception { + final X509Certificate in = caCertificate; + final String pem = CertUtils.x509CertificateToPem(in); + final X509Certificate out = CertUtils.pemToX509Certificate(pem); + Assert.assertTrue(pem.startsWith("-----BEGIN CERTIFICATE-----\n")); + Assert.assertTrue(pem.endsWith("-----END CERTIFICATE-----\n")); + Assert.assertEquals(in.getSerialNumber(), out.getSerialNumber()); + Assert.assertArrayEquals(in.getSignature(), out.getSignature()); + Assert.assertEquals(in.getSigAlgName(), out.getSigAlgName()); + Assert.assertEquals(in.getPublicKey(), out.getPublicKey()); + Assert.assertEquals(in.getNotBefore(), out.getNotBefore()); + Assert.assertEquals(in.getNotAfter(), out.getNotAfter()); + Assert.assertEquals(in.getIssuerDN().toString(), out.getIssuerDN().toString()); + } + + @Test + public void testKeysConversionMethods() throws Exception { + final KeyPair kp = CertUtils.generateRandomKeyPair(2048); + + final PrivateKey inPrivateKey = kp.getPrivate(); + final PrivateKey outPrivateKey = CertUtils.pemToPrivateKey(CertUtils.privateKeyToPem(inPrivateKey)); + Assert.assertEquals(inPrivateKey.getAlgorithm(), outPrivateKey.getAlgorithm()); + Assert.assertEquals(inPrivateKey.getFormat(), outPrivateKey.getFormat()); + Assert.assertArrayEquals(inPrivateKey.getEncoded(), outPrivateKey.getEncoded()); + + final PublicKey inPublicKey = kp.getPublic(); + final PublicKey outPublicKey = CertUtils.pemToPublicKey(CertUtils.publicKeyToPem(inPublicKey)); + Assert.assertEquals(inPublicKey.getAlgorithm(), outPublicKey.getAlgorithm()); + Assert.assertEquals(inPublicKey.getFormat(), inPublicKey.getFormat()); + Assert.assertArrayEquals(inPublicKey.getEncoded(), outPublicKey.getEncoded()); + } + + @Test + public void testGenerateRandomBigInt() throws Exception { + Assert.assertNotEquals(CertUtils.generateRandomBigInt(), CertUtils.generateRandomBigInt()); + } + + @Test + public void testGenerateCertificate() throws Exception { + final KeyPair clientKeyPair = CertUtils.generateRandomKeyPair(1024); + final List domainNames = Arrays.asList("domain1.com", "www.2.domain2.com", "3.domain3.com"); + final List addressList = Arrays.asList("1.2.3.4", "192.168.1.1", "2a02:120b:2c16:f6d0:d9df:8ebc:e44a:f181"); + + final X509Certificate clientCert = CertUtils.generateV3Certificate(caCertificate, caKeyPair.getPrivate(), clientKeyPair.getPublic(), + "CN=domain.example", "SHA256WithRSAEncryption", 10, domainNames, addressList); + + clientCert.verify(caKeyPair.getPublic()); + Assert.assertEquals(clientCert.getIssuerDN(), caCertificate.getIssuerDN()); + Assert.assertEquals(clientCert.getSigAlgName(), "SHA256WITHRSA"); + Assert.assertArrayEquals(clientCert.getPublicKey().getEncoded(), clientKeyPair.getPublic().getEncoded()); + Assert.assertNotNull(clientCert.getSubjectAlternativeNames()); + + for (final List altNames : clientCert.getSubjectAlternativeNames()) { + Assert.assertTrue(altNames.size() == 2); + final Object first = altNames.get(0); + final Object second = altNames.get(1); + if (first instanceof Integer && ((Integer) first) == GeneralName.iPAddress) { + Assert.assertTrue(addressList.contains((String) second)); + } + if (first instanceof Integer && ((Integer) first) == GeneralName.dNSName) { + Assert.assertTrue(domainNames.contains((String) second)); + } + } + } + +} \ No newline at end of file