Skip to content

Commit 7ce54bf

Browse files
authored
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 <rohit.yadav@shapeblue.com>
1 parent 64e56a2 commit 7ce54bf

124 files changed

Lines changed: 5151 additions & 756 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ env:
3434
matrix:
3535
- TESTS="smoke/test_affinity_groups
3636
smoke/test_affinity_groups_projects
37+
smoke/test_certauthority_root
3738
smoke/test_deploy_vgpu_enabled_vm
3839
smoke/test_deploy_vm_iso
3940
smoke/test_deploy_vm_root_resize

agent/src/com/cloud/agent/Agent.java

Lines changed: 121 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
// under the License.
1717
package com.cloud.agent;
1818

19+
import java.io.File;
1920
import java.io.IOException;
2021
import java.io.PrintWriter;
2122
import java.io.StringWriter;
2223
import java.net.InetAddress;
2324
import java.net.UnknownHostException;
2425
import java.nio.channels.ClosedChannelException;
26+
import java.nio.charset.Charset;
2527
import java.util.ArrayList;
2628
import java.util.List;
2729
import java.util.Map;
@@ -35,7 +37,13 @@
3537

3638
import javax.naming.ConfigurationException;
3739

40+
import org.apache.cloudstack.ca.SetupCertificateAnswer;
41+
import org.apache.cloudstack.ca.SetupCertificateCommand;
42+
import org.apache.cloudstack.ca.SetupKeyStoreCommand;
43+
import org.apache.cloudstack.ca.SetupKeystoreAnswer;
3844
import org.apache.cloudstack.managed.context.ManagedContextTimerTask;
45+
import org.apache.cloudstack.utils.security.KeyStoreUtils;
46+
import org.apache.commons.io.FileUtils;
3947
import org.apache.log4j.Logger;
4048
import org.slf4j.MDC;
4149

@@ -68,6 +76,7 @@
6876
import com.cloud.utils.nio.Task;
6977
import com.cloud.utils.script.OutputInterpreter;
7078
import com.cloud.utils.script.Script;
79+
import com.google.common.base.Strings;
7180

7281
/**
7382
* @config
@@ -126,6 +135,9 @@ public int value() {
126135
private final ThreadPoolExecutor _ugentTaskPool;
127136
ExecutorService _executor;
128137

138+
private String _keystoreSetupPath;
139+
private String _keystoreCertImportPath;
140+
129141
// for simulator use only
130142
public Agent(final IAgentShell shell) {
131143
_shell = shell;
@@ -166,7 +178,8 @@ public Agent(final IAgentShell shell, final int localAgentId, final ServerResour
166178
throw new ConfigurationException("Unable to configure " + _resource.getName());
167179
}
168180

169-
_connection = new NioClient("Agent", _shell.getHost(), _shell.getPort(), _shell.getWorkers(), this);
181+
final String host = _shell.getHost();
182+
_connection = new NioClient("Agent", host, _shell.getPort(), _shell.getWorkers(), this);
170183

171184
// ((NioClient)_connection).setBindAddress(_shell.getPrivateIp());
172185

@@ -182,7 +195,7 @@ public Agent(final IAgentShell shell, final int localAgentId, final ServerResour
182195
"agentRequest-Handler"));
183196

184197
s_logger.info("Agent [id = " + (_id != null ? _id : "new") + " : type = " + getResourceName() + " : zone = " + _shell.getZone() + " : pod = " + _shell.getPod() +
185-
" : workers = " + _shell.getWorkers() + " : host = " + _shell.getHost() + " : port = " + _shell.getPort());
198+
" : workers = " + _shell.getWorkers() + " : host = " + host + " : port = " + _shell.getPort());
186199
}
187200

188201
public String getVersion() {
@@ -224,15 +237,27 @@ public void start() {
224237
throw new CloudRuntimeException("Unable to start the resource: " + _resource.getName());
225238
}
226239

240+
_keystoreSetupPath = Script.findScript("scripts/util/", KeyStoreUtils.keyStoreSetupScript);
241+
if (_keystoreSetupPath == null) {
242+
throw new CloudRuntimeException(String.format("Unable to find the '%s' script", KeyStoreUtils.keyStoreSetupScript));
243+
}
244+
245+
_keystoreCertImportPath = Script.findScript("scripts/util/", KeyStoreUtils.keyStoreImportScript);
246+
if (_keystoreCertImportPath == null) {
247+
throw new CloudRuntimeException(String.format("Unable to find the '%s' script", KeyStoreUtils.keyStoreImportScript));
248+
}
249+
227250
try {
228251
_connection.start();
229252
} catch (final NioConnectionException e) {
230253
s_logger.warn("NIO Connection Exception " + e);
231254
s_logger.info("Attempted to connect to the server, but received an unexpected exception, trying again...");
232255
}
233256
while (!_connection.isStartup()) {
257+
final String host = _shell.getHost();
234258
_shell.getBackoffAlgorithm().waitBeforeRetry();
235-
_connection = new NioClient("Agent", _shell.getHost(), _shell.getPort(), _shell.getWorkers(), this);
259+
_connection = new NioClient("Agent", host, _shell.getPort(), _shell.getWorkers(), this);
260+
s_logger.info("Connecting to host:" + host);
236261
try {
237262
_connection.start();
238263
} catch (final NioConnectionException e) {
@@ -408,14 +433,21 @@ protected void reconnect(final Link link) {
408433
_shell.getBackoffAlgorithm().waitBeforeRetry();
409434
}
410435

411-
_connection = new NioClient("Agent", _shell.getHost(), _shell.getPort(), _shell.getWorkers(), this);
436+
final String host = _shell.getHost();
412437
do {
413-
s_logger.info("Reconnecting...");
438+
_connection = new NioClient("Agent", host, _shell.getPort(), _shell.getWorkers(), this);
439+
s_logger.info("Reconnecting to host:" + host);
414440
try {
415441
_connection.start();
416442
} catch (final NioConnectionException e) {
417443
s_logger.warn("NIO Connection Exception " + e);
418444
s_logger.info("Attempted to connect to the server, but received an unexpected exception, trying again...");
445+
_connection.stop();
446+
try {
447+
_connection.cleanUp();
448+
} catch (final IOException ex) {
449+
s_logger.warn("Fail to clean up old connection. " + ex);
450+
}
419451
}
420452
_shell.getBackoffAlgorithm().waitBeforeRetry();
421453
} while (!_connection.isStartup());
@@ -515,7 +547,10 @@ protected void processRequest(final Request request, final Link link) {
515547
s_logger.warn("No handler found to process cmd: " + cmd.toString());
516548
answer = new AgentControlAnswer(cmd);
517549
}
518-
550+
} else if (cmd instanceof SetupKeyStoreCommand && ((SetupKeyStoreCommand) cmd).isHandleByAgent()) {
551+
answer = setupAgentKeystore((SetupKeyStoreCommand) cmd);
552+
} else if (cmd instanceof SetupCertificateCommand && ((SetupCertificateCommand) cmd).isHandleByAgent()) {
553+
answer = setupAgentCertificate((SetupCertificateCommand) cmd);
519554
} else {
520555
if (cmd instanceof ReadyCommand) {
521556
processReadyCommand(cmd);
@@ -565,6 +600,86 @@ protected void processRequest(final Request request, final Link link) {
565600
}
566601
}
567602

603+
public Answer setupAgentKeystore(final SetupKeyStoreCommand cmd) {
604+
final String keyStorePassword = cmd.getKeystorePassword();
605+
final long validityDays = cmd.getValidityDays();
606+
607+
s_logger.debug("Setting up agent keystore file and generating CSR");
608+
609+
final File agentFile = PropertiesUtil.findConfigFile("agent.properties");
610+
if (agentFile == null) {
611+
return new Answer(cmd, false, "Failed to find agent.properties file");
612+
}
613+
final String keyStoreFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultKeystoreFile;
614+
final String csrFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultCsrFile;
615+
616+
String storedPassword = _shell.getPersistentProperty(null, KeyStoreUtils.passphrasePropertyName);
617+
if (Strings.isNullOrEmpty(storedPassword)) {
618+
storedPassword = keyStorePassword;
619+
_shell.setPersistentProperty(null, KeyStoreUtils.passphrasePropertyName, storedPassword);
620+
}
621+
622+
Script script = new Script(_keystoreSetupPath, 60000, s_logger);
623+
script.add(agentFile.getAbsolutePath());
624+
script.add(keyStoreFile);
625+
script.add(storedPassword);
626+
script.add(String.valueOf(validityDays));
627+
script.add(csrFile);
628+
String result = script.execute();
629+
if (result != null) {
630+
throw new CloudRuntimeException("Unable to setup keystore file");
631+
}
632+
633+
final String csrString;
634+
try {
635+
csrString = FileUtils.readFileToString(new File(csrFile), Charset.defaultCharset());
636+
} catch (IOException e) {
637+
throw new CloudRuntimeException("Unable to read generated CSR file", e);
638+
}
639+
return new SetupKeystoreAnswer(csrString);
640+
}
641+
642+
private Answer setupAgentCertificate(final SetupCertificateCommand cmd) {
643+
final String certificate = cmd.getCertificate();
644+
final String privateKey = cmd.getPrivateKey();
645+
final String caCertificates = cmd.getCaCertificates();
646+
647+
s_logger.debug("Importing received certificate to agent's keystore");
648+
649+
final File agentFile = PropertiesUtil.findConfigFile("agent.properties");
650+
if (agentFile == null) {
651+
return new Answer(cmd, false, "Failed to find agent.properties file");
652+
}
653+
final String keyStoreFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultKeystoreFile;
654+
final String certFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultCertFile;
655+
final String privateKeyFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultPrivateKeyFile;
656+
final String caCertFile = agentFile.getParent() + "/" + KeyStoreUtils.defaultCaCertFile;
657+
658+
try {
659+
FileUtils.writeStringToFile(new File(certFile), certificate, Charset.defaultCharset());
660+
FileUtils.writeStringToFile(new File(caCertFile), caCertificates, Charset.defaultCharset());
661+
s_logger.debug("Saved received client certificate to: " + certFile);
662+
} catch (IOException e) {
663+
throw new CloudRuntimeException("Unable to save received agent client and ca certificates", e);
664+
}
665+
666+
Script script = new Script(_keystoreCertImportPath, 60000, s_logger);
667+
script.add(agentFile.getAbsolutePath());
668+
script.add(keyStoreFile);
669+
script.add(KeyStoreUtils.agentMode);
670+
script.add(certFile);
671+
script.add("");
672+
script.add(caCertFile);
673+
script.add("");
674+
script.add(privateKeyFile);
675+
script.add(privateKey);
676+
String result = script.execute();
677+
if (result != null) {
678+
throw new CloudRuntimeException("Unable to import certificate into keystore file");
679+
}
680+
return new SetupCertificateAnswer(true);
681+
}
682+
568683
public void processResponse(final Response response, final Link link) {
569684
final Answer answer = response.getAnswer();
570685
if (s_logger.isDebugEnabled()) {

agent/src/com/cloud/agent/AgentShell.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public class AgentShell implements IAgentShell, Daemon {
6767
private int _proxyPort;
6868
private int _workers;
6969
private String _guid;
70+
private int _hostCounter = 0;
7071
private int _nextAgentId = 1;
7172
private volatile boolean _exit = false;
7273
private int _pingRetries;
@@ -107,7 +108,17 @@ public String getPod() {
107108

108109
@Override
109110
public String getHost() {
110-
return _host;
111+
final String[] hosts = _host.split(",");
112+
if (_hostCounter >= hosts.length) {
113+
_hostCounter = 0;
114+
}
115+
final String host = hosts[_hostCounter % hosts.length];
116+
_hostCounter++;
117+
return host;
118+
}
119+
120+
public void setHost(final String host) {
121+
_host = host;
111122
}
112123

113124
@Override

agent/src/com/cloud/agent/dao/impl/PropertiesStorage.java

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ public synchronized String get(String key) {
5151

5252
@Override
5353
public synchronized void persist(String key, String value) {
54+
if (!loadFromFile(_file)) {
55+
s_logger.error("Failed to load changes and then write to them");
56+
}
5457
_properties.setProperty(key, value);
5558
FileOutputStream output = null;
5659
try {
@@ -65,6 +68,20 @@ public synchronized void persist(String key, String value) {
6568
}
6669
}
6770

71+
private synchronized boolean loadFromFile(final File file) {
72+
try {
73+
PropertiesUtil.loadFromFile(_properties, file);
74+
_file = file;
75+
} catch (FileNotFoundException e) {
76+
s_logger.error("How did we get here? ", e);
77+
return false;
78+
} catch (IOException e) {
79+
s_logger.error("IOException: ", e);
80+
return false;
81+
}
82+
return true;
83+
}
84+
6885
@Override
6986
public synchronized boolean configure(String name, Map<String, Object> params) {
7087
_name = name;
@@ -86,17 +103,7 @@ public synchronized boolean configure(String name, Map<String, Object> params) {
86103
return false;
87104
}
88105
}
89-
try {
90-
PropertiesUtil.loadFromFile(_properties, file);
91-
_file = file;
92-
} catch (FileNotFoundException e) {
93-
s_logger.error("How did we get here? ", e);
94-
return false;
95-
} catch (IOException e) {
96-
s_logger.error("IOException: ", e);
97-
return false;
98-
}
99-
return true;
106+
return loadFromFile(file);
100107
}
101108

102109
@Override

agent/src/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,8 @@
3232

3333
import javax.naming.ConfigurationException;
3434

35-
import org.apache.log4j.Logger;
36-
37-
import com.google.gson.Gson;
38-
3935
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
36+
import org.apache.log4j.Logger;
4037

4138
import com.cloud.agent.Agent.ExitStatus;
4239
import com.cloud.agent.api.AgentControlAnswer;
@@ -64,6 +61,7 @@
6461
import com.cloud.utils.NumbersUtil;
6562
import com.cloud.utils.net.NetUtils;
6663
import com.cloud.utils.script.Script;
64+
import com.google.gson.Gson;
6765

6866
/**
6967
*
@@ -240,9 +238,11 @@ public boolean configure(String name, Map<String, Object> params) throws Configu
240238
_proxyVmId = NumbersUtil.parseLong(value, 0);
241239

242240
if (_localgw != null) {
243-
String mgmtHost = (String)params.get("host");
241+
String mgmtHosts = (String)params.get("host");
244242
if (_eth1ip != null) {
245-
addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost);
243+
for (final String mgmtHost : mgmtHosts.split(",")) {
244+
addRouteToInternalIpOrCidr(_localgw, _eth1ip, _eth1mask, mgmtHost);
245+
}
246246
String internalDns1 = (String) params.get("internaldns1");
247247
if (internalDns1 == null) {
248248
s_logger.warn("No DNS entry found during configuration of NfsSecondaryStorage");

agent/test/com/cloud/agent/AgentShellTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@
1616
// under the License.
1717
package com.cloud.agent;
1818

19+
import java.util.Arrays;
20+
import java.util.List;
1921
import java.util.UUID;
2022

2123
import javax.naming.ConfigurationException;
2224

2325
import org.junit.Assert;
2426
import org.junit.Test;
2527

28+
import com.cloud.utils.StringUtils;
29+
2630
public class AgentShellTest {
2731
@Test
2832
public void parseCommand() throws ConfigurationException {
@@ -44,4 +48,15 @@ public void loadProperties() throws ConfigurationException {
4448
Assert.assertNotNull(shell.getProperties());
4549
Assert.assertFalse(shell.getProperties().entrySet().isEmpty());
4650
}
51+
52+
@Test
53+
public void testGetHost() {
54+
AgentShell shell = new AgentShell();
55+
List<String> hosts = Arrays.asList("10.1.1.1", "20.2.2.2", "30.3.3.3", "2001:db8::1");
56+
shell.setHost(StringUtils.listToCsvTags(hosts));
57+
for (String host : hosts) {
58+
Assert.assertEquals(host, shell.getHost());
59+
}
60+
Assert.assertEquals(shell.getHost(), hosts.get(0));
61+
}
4762
}

api/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@
5151
<artifactId>cloud-framework-config</artifactId>
5252
<version>${project.version}</version>
5353
</dependency>
54+
<dependency>
55+
<groupId>org.apache.cloudstack</groupId>
56+
<artifactId>cloud-framework-ca</artifactId>
57+
<version>${project.version}</version>
58+
</dependency>
5459
</dependencies>
5560
<build>
5661
<plugins>

0 commit comments

Comments
 (0)