Improvement: SSL offloading with Virtual Router (#11468)
* SSL offloading with Virtual Router
* PR11468: fix pre-commit errors
* PR11468: api->getAPI/postAPI in UI
* SSL: add smoke tests for VPC in user project
* PR11468: address Daan's comments
* Fix test/integration/smoke/test_ssl_offloading.py
* SSL: remove ssl certificates when clean up account
* SSL offloading: add unit tests
* SSL offloading: UI fixes part 1
* SSL offloading: UI changes part 2
* SSL offloading: add more unit tests
* SSL offloading: more unit tests 3
* SSL offloading: wrong check
* SSL offloading: more and more unit tests
* SSL offloading: add testUpdateLoadBalancerRule5
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 32b7014..5c79d1d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -48,6 +48,7 @@
exclude: >
(?x)
^scripts/vm/systemvm/id_rsa\.cloud$|
+ ^server/src/test/java/org/apache/cloudstack/network/ssl/CertServiceTest.java$|
^server/src/test/java/com/cloud/keystore/KeystoreTest\.java$|
^server/src/test/resources/certs/dsa_self_signed\.key$|
^server/src/test/resources/certs/non_root\.key$|
@@ -57,7 +58,8 @@
^server/src/test/resources/certs/rsa_self_signed\.key$|
^services/console-proxy/rdpconsole/src/test/doc/rdp-key\.pem$|
^systemvm/agent/certs/localhost\.key$|
- ^systemvm/agent/certs/realhostip\.key$
+ ^systemvm/agent/certs/realhostip\.key$|
+ ^test/integration/smoke/test_ssl_offloading.py$
- id: end-of-file-fixer
exclude: \.vhd$
- id: fix-byte-order-marker
@@ -75,7 +77,7 @@
name: run codespell
description: Check spelling with codespell
args: [--ignore-words=.github/linters/codespell.txt]
- exclude: ^systemvm/agent/noVNC/|^ui/package\.json$|^ui/package-lock\.json$|^ui/public/js/less\.min\.js$|^ui/public/locales/.*[^n].*\.json$
+ exclude: ^systemvm/agent/noVNC/|^ui/package\.json$|^ui/package-lock\.json$|^ui/public/js/less\.min\.js$|^ui/public/locales/.*[^n].*\.json$|^server/src/test/java/org/apache/cloudstack/network/ssl/CertServiceTest.java$|^test/integration/smoke/test_ssl_offloading.py$
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
diff --git a/api/src/main/java/com/cloud/agent/api/to/LoadBalancerTO.java b/api/src/main/java/com/cloud/agent/api/to/LoadBalancerTO.java
index f395f26..6c4b9e6 100644
--- a/api/src/main/java/com/cloud/agent/api/to/LoadBalancerTO.java
+++ b/api/src/main/java/com/cloud/agent/api/to/LoadBalancerTO.java
@@ -71,7 +71,7 @@
this.destinations = new DestinationTO[destinations.size()];
this.stickinessPolicies = null;
this.sslCert = null;
- this.lbProtocol = null;
+ this.lbProtocol = protocol;
int i = 0;
for (LbDestination destination : destinations) {
this.destinations[i++] = new DestinationTO(destination.getIpAddress(), destination.getDestinationPortStart(), destination.isRevoked(), false);
@@ -205,6 +205,10 @@
return this.sslCert;
}
+ public void setLbSslCert(LbSslCert sslCert) {
+ this.sslCert = sslCert;
+ }
+
public String getSrcIpVlan() {
return srcIpVlan;
}
diff --git a/api/src/main/java/com/cloud/network/lb/LoadBalancingRulesService.java b/api/src/main/java/com/cloud/network/lb/LoadBalancingRulesService.java
index 46f1723..3fc6028 100644
--- a/api/src/main/java/com/cloud/network/lb/LoadBalancingRulesService.java
+++ b/api/src/main/java/com/cloud/network/lb/LoadBalancingRulesService.java
@@ -106,7 +106,7 @@
boolean applyLoadBalancerConfig(long lbRuleId) throws ResourceUnavailableException;
- boolean assignCertToLoadBalancer(long lbRuleId, Long certId);
+ boolean assignCertToLoadBalancer(long lbRuleId, Long certId, boolean isForced);
boolean removeCertFromLoadBalancer(long lbRuleId);
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignCertToLoadBalancerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignCertToLoadBalancerCmd.java
index 4f9d2f3..bfc1554 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignCertToLoadBalancerCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignCertToLoadBalancerCmd.java
@@ -27,6 +27,7 @@
import org.apache.cloudstack.api.response.FirewallRuleResponse;
import org.apache.cloudstack.api.response.SslCertResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
+import org.apache.commons.lang3.BooleanUtils;
import com.cloud.event.EventTypes;
import com.cloud.exception.ConcurrentOperationException;
@@ -57,11 +58,17 @@
description = "the ID of the certificate")
Long certId;
+ @Parameter(name = ApiConstants.FORCED,
+ type = CommandType.BOOLEAN,
+ since = "4.22",
+ description = "Force assign the certificate. If there is a certificate assigned to the LB, it will be removed at first.")
+ private Boolean forced;
+
@Override
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException,
ResourceAllocationException, NetworkRuleConflictException {
//To change body of implemented methods use File | Settings | File Templates.
- if (_lbService.assignCertToLoadBalancer(getLbRuleId(), getCertId())) {
+ if (_lbService.assignCertToLoadBalancer(getLbRuleId(), getCertId(), isForced())) {
SuccessResponse response = new SuccessResponse(getCommandName());
this.setResponseObject(response);
} else {
@@ -95,4 +102,19 @@
public Long getLbRuleId() {
return lbRuleId;
}
+
+ public boolean isForced() {
+ return BooleanUtils.toBoolean(forced);
+ }
+
+ @Override
+ public String getSyncObjType() {
+ return BaseAsyncCmd.networkSyncObject;
+ }
+
+ @Override
+ public Long getSyncObjId() {
+ LoadBalancer lb = _entityMgr.findById(LoadBalancer.class, getLbRuleId());
+ return (lb != null)? lb.getNetworkId(): null;
+ }
}
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLoadBalancerRuleCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLoadBalancerRuleCmd.java
index 34798c4..aa43b9c 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLoadBalancerRuleCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLoadBalancerRuleCmd.java
@@ -33,6 +33,7 @@
import org.apache.cloudstack.api.response.NetworkResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.context.CallContext;
+import org.apache.commons.lang3.StringUtils;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenter.NetworkType;
@@ -112,7 +113,7 @@
+ "rule will be created for. Required when public Ip address is not associated with any Guest network yet (VPC case)")
private Long networkId;
- @Parameter(name = ApiConstants.PROTOCOL, type = CommandType.STRING, description = "The protocol for the LB such as tcp, udp or tcp-proxy.")
+ @Parameter(name = ApiConstants.PROTOCOL, type = CommandType.STRING, description = "The protocol for the LB such as tcp, udp, tcp-proxy or ssl.")
private String lbProtocol;
@Parameter(name = ApiConstants.FOR_DISPLAY, type = CommandType.BOOLEAN, description = "an optional field, whether to the display the rule to the end user or not", since = "4.4", authorized = {RoleType.Admin})
@@ -253,7 +254,7 @@
}
public String getLbProtocol() {
- return lbProtocol;
+ return StringUtils.trim(StringUtils.lowerCase(lbProtocol));
}
/////////////////////////////////////////////////////
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/RemoveCertFromLoadBalancerCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/RemoveCertFromLoadBalancerCmd.java
index dfaafe8..ddd2133 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/RemoveCertFromLoadBalancerCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/RemoveCertFromLoadBalancerCmd.java
@@ -82,4 +82,15 @@
public Long getLbRuleId() {
return this.lbRuleId;
}
+
+ @Override
+ public String getSyncObjType() {
+ return BaseAsyncCmd.networkSyncObject;
+ }
+
+ @Override
+ public Long getSyncObjId() {
+ LoadBalancer lb = _entityMgr.findById(LoadBalancer.class, getLbRuleId());
+ return (lb != null)? lb.getNetworkId(): null;
+ }
}
diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/LoadBalancerConfigItem.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/LoadBalancerConfigItem.java
index 4832c90..6dae886 100644
--- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/LoadBalancerConfigItem.java
+++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/facade/LoadBalancerConfigItem.java
@@ -56,6 +56,8 @@
final String[] statRules = allRules[LoadBalancerConfigurator.STATS];
final LoadBalancerRule loadBalancerRule = new LoadBalancerRule(configuration, tmpCfgFilePath, tmpCfgFileName, addRules, removeRules, statRules, routerIp);
+ final LoadBalancerRule.SslCertEntry[] sslCerts = cfgtr.generateSslCertEntries(command);
+ loadBalancerRule.setSslCerts(sslCerts);
final List<LoadBalancerRule> rules = new LinkedList<LoadBalancerRule>();
rules.add(loadBalancerRule);
diff --git a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/LoadBalancerRule.java b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/LoadBalancerRule.java
index 3743d60..361c476 100644
--- a/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/LoadBalancerRule.java
+++ b/core/src/main/java/com/cloud/agent/resource/virtualnetwork/model/LoadBalancerRule.java
@@ -25,6 +25,7 @@
private String[] configuration;
private String tmpCfgFilePath;
private String tmpCfgFileName;
+ private SslCertEntry[] sslCerts;
private String[] addRules;
private String[] removeRules;
@@ -32,6 +33,53 @@
private String routerIp;
+ public static class SslCertEntry {
+ private String name;
+ private String cert;
+ private String key;
+ private String chain;
+ private String password;
+
+ public SslCertEntry(String name, String cert, String key, String chain, String password) {
+ this.name = name;
+ this.cert = cert;
+ this.key = key;
+ this.chain = chain;
+ this.password = password;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ public String getName() {
+ return name;
+ }
+ public void setCert(String cert) {
+ this.cert = cert;
+ }
+ public String getCert() {
+ return cert;
+ }
+ public void setKey(String key) {
+ this.key = key;
+ }
+ public String getKey() {
+ return key;
+ }
+ public void setChain(String chain) {
+ this.chain = chain;
+ }
+ public String getChain() {
+ return chain;
+ }
+ public void setPassword(String password) {
+ this.password = password;
+ }
+ public String getPassword() {
+ return password;
+ }
+ }
+
public LoadBalancerRule() {
// Empty constructor for (de)serialization
}
@@ -101,4 +149,12 @@
public void setRouterIp(final String routerIp) {
this.routerIp = routerIp;
}
+
+ public SslCertEntry[] getSslCerts() {
+ return sslCerts;
+ }
+
+ public void setSslCerts(final SslCertEntry[] sslCerts) {
+ this.sslCerts = sslCerts;
+ }
}
diff --git a/core/src/main/java/com/cloud/network/HAProxyConfigurator.java b/core/src/main/java/com/cloud/network/HAProxyConfigurator.java
index e4b0a7f..7736bea 100644
--- a/core/src/main/java/com/cloud/network/HAProxyConfigurator.java
+++ b/core/src/main/java/com/cloud/network/HAProxyConfigurator.java
@@ -36,6 +36,8 @@
import com.cloud.agent.api.to.LoadBalancerTO.DestinationTO;
import com.cloud.agent.api.to.LoadBalancerTO.StickinessPolicyTO;
import com.cloud.agent.api.to.PortForwardingRuleTO;
+import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRule.SslCertEntry;
+import com.cloud.network.lb.LoadBalancingRule.LbSslCert;
import com.cloud.network.rules.LbStickinessMethod.StickinessMethodType;
import com.cloud.utils.Pair;
import com.cloud.utils.net.NetUtils;
@@ -52,6 +54,12 @@
private static String[] defaultListen = {"listen vmops", "\tbind 0.0.0.0:9", "\toption transparent"};
+ private static final String SSL_CERTS_DIR = "/etc/cloudstack/ssl/";
+
+ private static final String SSL_CONFIGURATION_INTERMEDIATE = " ssl-min-ver TLSv1.2 no-tls-tickets " +
+ "ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-GCM-SHA256 " +
+ "ciphersuites TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256";
+
@Override
public String[] generateConfiguration(final List<PortForwardingRuleTO> fwRules) {
// Group the rules by publicip:publicport
@@ -469,30 +477,41 @@
return sb.toString();
}
- private List<String> getRulesForPool(final LoadBalancerTO lbTO, final boolean keepAliveEnabled) {
+ private List<String> getRulesForPool(final LoadBalancerTO lbTO, final LoadBalancerConfigCommand lbCmd) {
StringBuilder sb = new StringBuilder();
final String poolName = sb.append(lbTO.getSrcIp().replace(".", "_")).append('-').append(lbTO.getSrcPort()).toString();
final String publicIP = lbTO.getSrcIp();
final int publicPort = lbTO.getSrcPort();
final String algorithm = lbTO.getAlgorithm();
- final List<String> result = new ArrayList<String>();
- // add line like this: "listen 65_37_141_30-80\n\tbind 65.37.141.30:80"
- sb = new StringBuilder();
- sb.append("listen ").append(poolName);
- result.add(sb.toString());
+ boolean sslOffloading = lbTO.getSslCert() != null && !lbTO.getSslCert().isRevoked()
+ && NetUtils.SSL_PROTO.equals(lbTO.getLbProtocol());
+
+ final List<String> frontendConfigs = new ArrayList<>();
+ final List<String> backendConfigs = new ArrayList<>();
+ final List<String> result = new ArrayList<>();
+
sb = new StringBuilder();
sb.append("\tbind ").append(publicIP).append(":").append(publicPort);
- result.add(sb.toString());
+
+ if (sslOffloading) {
+ sb.append(" ssl crt ").append(SSL_CERTS_DIR).append(poolName).append(".pem");
+ // check for http2 support
+ sb.append(" alpn h2,http/1.1");
+ sb.append(SSL_CONFIGURATION_INTERMEDIATE);
+ sb.append("\n\thttp-request add-header X-Forwarded-Proto https");
+ }
+ frontendConfigs.add(sb.toString());
+
sb = new StringBuilder();
sb.append("\t").append("balance ").append(algorithm.toLowerCase());
- result.add(sb.toString());
+ backendConfigs.add(sb.toString());
int i = 0;
- Boolean destsAvailable = false;
+ boolean destsAvailable = false;
final String stickinessSubRule = getLbSubRuleForStickiness(lbTO);
- final List<String> dstSubRule = new ArrayList<String>();
- final List<String> dstWithCookieSubRule = new ArrayList<String>();
+ final List<String> dstSubRule = new ArrayList<>();
+ final List<String> dstWithCookieSubRule = new ArrayList<>();
for (final DestinationTO dest : lbTO.getDestinations()) {
// add line like this: "server 65_37_141_30-80_3 10.1.1.4:80 check"
if (dest.isRevoked()) {
@@ -500,15 +519,20 @@
}
sb = new StringBuilder();
sb.append("\t")
- .append("server ")
- .append(poolName)
- .append("_")
- .append(Integer.toString(i++))
- .append(" ")
- .append(dest.getDestIp())
- .append(":")
- .append(dest.getDestPort())
- .append(" check");
+ .append("server ")
+ .append(poolName)
+ .append("_")
+ .append(i++)
+ .append(" ")
+ .append(dest.getDestIp())
+ .append(":")
+ .append(dest.getDestPort())
+ .append(" check");
+
+ if (sslOffloading) {
+ sb.append(SSL_CONFIGURATION_INTERMEDIATE);
+ }
+
if(lbTO.getLbProtocol() != null && lbTO.getLbProtocol().equals("tcp-proxy")) {
sb.append(" send-proxy");
}
@@ -520,9 +544,9 @@
destsAvailable = true;
}
- Boolean httpbasedStickiness = false;
+ boolean httpbasedStickiness = false;
/* attach stickiness sub rule only if the destinations are available */
- if (stickinessSubRule != null && destsAvailable == true) {
+ if (stickinessSubRule != null && destsAvailable) {
for (final StickinessPolicyTO stickinessPolicy : lbTO.getStickinessPolicies()) {
if (stickinessPolicy == null) {
continue;
@@ -530,35 +554,40 @@
if (StickinessMethodType.LBCookieBased.getName().equalsIgnoreCase(stickinessPolicy.getMethodName()) ||
StickinessMethodType.AppCookieBased.getName().equalsIgnoreCase(stickinessPolicy.getMethodName())) {
httpbasedStickiness = true;
+ break;
}
}
if (httpbasedStickiness) {
- result.addAll(dstWithCookieSubRule);
+ backendConfigs.addAll(dstWithCookieSubRule);
} else {
- result.addAll(dstSubRule);
+ backendConfigs.addAll(dstSubRule);
}
- result.add(stickinessSubRule);
+ backendConfigs.add(stickinessSubRule);
} else {
- result.addAll(dstSubRule);
+ backendConfigs.addAll(dstSubRule);
}
if (stickinessSubRule != null && !destsAvailable) {
logger.warn("Haproxy stickiness policy for lb rule: " + lbTO.getSrcIp() + ":" + lbTO.getSrcPort() + ": Not Applied, cause: backends are unavailable");
}
- if (publicPort == NetUtils.HTTP_PORT && !keepAliveEnabled || httpbasedStickiness) {
- sb = new StringBuilder();
- sb.append("\t").append("mode http");
- result.add(sb.toString());
- sb = new StringBuilder();
- sb.append("\t").append("option httpclose");
- result.add(sb.toString());
+ boolean keepAliveEnabled = lbCmd.keepAliveEnabled;
+ boolean http = (publicPort == NetUtils.HTTP_PORT && !keepAliveEnabled);
+ if (http || httpbasedStickiness || sslOffloading) {
+ frontendConfigs.add("\tmode http");
+ String keepAliveLine = keepAliveEnabled ? "\tno option forceclose" : "\toption httpclose";
+ frontendConfigs.add(keepAliveLine);
}
+ // add line like this: "listen 65_37_141_30-80\n\tbind 65.37.141.30:80"
+ result.add(String.format("listen %s", poolName));
+ result.addAll(frontendConfigs);
+
String cidrList = lbTO.getCidrList();
if (StringUtils.isNotBlank(cidrList)) {
result.add(String.format("\tacl network_allowed src %s \n\ttcp-request connection reject if !network_allowed", cidrList));
}
+ result.addAll(backendConfigs);
result.add(blankLine);
return result;
}
@@ -566,15 +595,18 @@
private String generateStatsRule(final LoadBalancerConfigCommand lbCmd, final String ruleName, final String statsIp) {
final StringBuilder rule = new StringBuilder("\nlisten ").append(ruleName).append("\n\tbind ").append(statsIp).append(":").append(lbCmd.lbStatsPort);
// TODO DH: write test for this in both cases
- if (!lbCmd.keepAliveEnabled) {
- logger.info("Haproxy mode http enabled");
- rule.append("\n\tmode http\n\toption httpclose");
+ rule.append("\n\tmode http");
+ if (lbCmd.keepAliveEnabled) {
+ logger.info("Haproxy option http-keep-alive enabled");
+ } else {
+ logger.info("Haproxy option httpclose enabled");
+ rule.append("\n\toption httpclose");
}
rule.append("\n\tstats enable\n\tstats uri ")
- .append(lbCmd.lbStatsUri)
- .append("\n\tstats realm Haproxy\\ Statistics\n\tstats auth ")
- .append(lbCmd.lbStatsAuth);
- rule.append("\n");
+ .append(lbCmd.lbStatsUri)
+ .append("\n\tstats realm Haproxy\\ Statistics\n\tstats auth ")
+ .append(lbCmd.lbStatsAuth)
+ .append("\n");
final String result = rule.toString();
if (logger.isDebugEnabled()) {
logger.debug("Haproxystats rule: " + result);
@@ -644,7 +676,7 @@
if (lbTO.isRevoked()) {
continue;
}
- final List<String> poolRules = getRulesForPool(lbTO, lbCmd.keepAliveEnabled);
+ final List<String> poolRules = getRulesForPool(lbTO, lbCmd);
result.addAll(poolRules);
has_listener = true;
}
@@ -696,4 +728,30 @@
return result;
}
+
+ @Override
+ public SslCertEntry[] generateSslCertEntries(LoadBalancerConfigCommand lbCmd) {
+ final Set<SslCertEntry> sslCertEntries = new HashSet<>();
+ for (final LoadBalancerTO lbTO : lbCmd.getLoadBalancers()) {
+ if (lbTO.getSslCert() != null) {
+ addSslCertEntry(sslCertEntries, lbTO);
+ }
+ }
+ final SslCertEntry[] result = sslCertEntries.toArray(new SslCertEntry[sslCertEntries.size()]);
+ return result;
+ }
+
+ private void addSslCertEntry(Set<SslCertEntry> sslCertEntries, LoadBalancerTO lbTO) {
+ final LbSslCert cert = lbTO.getSslCert();
+ if (cert.isRevoked()) {
+ return;
+ }
+ if (lbTO.getLbProtocol() == null || ! lbTO.getLbProtocol().equals(NetUtils.SSL_PROTO)) {
+ return;
+ }
+ StringBuilder sb = new StringBuilder();
+ final String name = sb.append(lbTO.getSrcIp().replace(".", "_")).append('-').append(lbTO.getSrcPort()).toString();
+ final SslCertEntry sslCertEntry = new SslCertEntry(name, cert.getCert(), cert.getKey(), cert.getChain(), cert.getPassword());
+ sslCertEntries.add(sslCertEntry);
+ }
}
diff --git a/core/src/main/java/com/cloud/network/LoadBalancerConfigurator.java b/core/src/main/java/com/cloud/network/LoadBalancerConfigurator.java
index 0e19b1e..8814f60 100644
--- a/core/src/main/java/com/cloud/network/LoadBalancerConfigurator.java
+++ b/core/src/main/java/com/cloud/network/LoadBalancerConfigurator.java
@@ -23,6 +23,7 @@
import com.cloud.agent.api.routing.LoadBalancerConfigCommand;
import com.cloud.agent.api.to.PortForwardingRuleTO;
+import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRule.SslCertEntry;
public interface LoadBalancerConfigurator {
public final static int ADD = 0;
@@ -34,4 +35,6 @@
public String[] generateConfiguration(LoadBalancerConfigCommand lbCmd);
public String[][] generateFwRules(LoadBalancerConfigCommand lbCmd);
+
+ public SslCertEntry[] generateSslCertEntries(LoadBalancerConfigCommand lbCmd);
}
diff --git a/core/src/test/java/com/cloud/agent/resource/virtualnetwork/ConfigHelperTest.java b/core/src/test/java/com/cloud/agent/resource/virtualnetwork/ConfigHelperTest.java
index 042bec9d..6d4c623 100644
--- a/core/src/test/java/com/cloud/agent/resource/virtualnetwork/ConfigHelperTest.java
+++ b/core/src/test/java/com/cloud/agent/resource/virtualnetwork/ConfigHelperTest.java
@@ -57,6 +57,7 @@
import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRule;
import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRules;
import com.cloud.network.lb.LoadBalancingRule.LbDestination;
+import com.cloud.network.lb.LoadBalancingRule.LbSslCert;
import com.cloud.network.Networks.TrafficType;
public class ConfigHelperTest {
@@ -223,9 +224,12 @@
protected LoadBalancerConfigCommand generateLoadBalancerConfigCommand() {
final List<LoadBalancerTO> lbs = new ArrayList<>();
final List<LbDestination> dests = new ArrayList<>();
+ final LbSslCert lbSslCert = new LbSslCert("cert", "key", "password", "chain", "fingerprint", false);
dests.add(new LbDestination(80, 8080, "10.1.10.2", false));
dests.add(new LbDestination(80, 8080, "10.1.10.2", true));
- lbs.add(new LoadBalancerTO(UUID.randomUUID().toString(), "64.10.1.10", 80, "tcp", "algo", false, false, false, dests));
+ LoadBalancerTO loadBalancerTO = new LoadBalancerTO(UUID.randomUUID().toString(), "64.10.1.10", 80, "tcp", "algo", false, false, false, dests);
+ loadBalancerTO.setLbSslCert(lbSslCert);
+ lbs.add(loadBalancerTO);
final LoadBalancerTO[] arrayLbs = new LoadBalancerTO[lbs.size()];
lbs.toArray(arrayLbs);
diff --git a/core/src/test/java/com/cloud/agent/resource/virtualnetwork/model/LoadBalancerRuleTest.java b/core/src/test/java/com/cloud/agent/resource/virtualnetwork/model/LoadBalancerRuleTest.java
new file mode 100644
index 0000000..f2b25a5
--- /dev/null
+++ b/core/src/test/java/com/cloud/agent/resource/virtualnetwork/model/LoadBalancerRuleTest.java
@@ -0,0 +1,63 @@
+// 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.agent.resource.virtualnetwork.model;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class LoadBalancerRuleTest {
+
+ @Test
+ public void testSslCertEntry() {
+ String name = "name";
+ String cert = "cert";
+ String key = "key1";
+ String chain = "chain";
+ String password = "password";
+ LoadBalancerRule.SslCertEntry sslCertEntry = new LoadBalancerRule.SslCertEntry(name, cert, key, chain, password);
+
+ Assert.assertEquals(name, sslCertEntry.getName());
+ Assert.assertEquals(cert, sslCertEntry.getCert());
+ Assert.assertEquals(key, sslCertEntry.getKey());
+ Assert.assertEquals(chain, sslCertEntry.getChain());
+ Assert.assertEquals(password, sslCertEntry.getPassword());
+
+ String name2 = "name2";
+ String cert2 = "cert2";
+ String key2 = "key2";
+ String chain2 = "chain2";
+ String password2 = "password2";
+
+ sslCertEntry.setName(name2);
+ sslCertEntry.setCert(cert2);
+ sslCertEntry.setKey(key2);
+ sslCertEntry.setChain(chain2);
+ sslCertEntry.setPassword(password2);
+
+ Assert.assertEquals(name2, sslCertEntry.getName());
+ Assert.assertEquals(cert2, sslCertEntry.getCert());
+ Assert.assertEquals(key2, sslCertEntry.getKey());
+ Assert.assertEquals(chain2, sslCertEntry.getChain());
+ Assert.assertEquals(password2, sslCertEntry.getPassword());
+
+ LoadBalancerRule loadBalancerRule = new LoadBalancerRule();
+ loadBalancerRule.setSslCerts(new LoadBalancerRule.SslCertEntry[]{sslCertEntry});
+
+ Assert.assertEquals(1, loadBalancerRule.getSslCerts().length);
+ Assert.assertEquals(sslCertEntry, loadBalancerRule.getSslCerts()[0]);
+ }
+}
diff --git a/core/src/test/java/com/cloud/network/HAProxyConfiguratorTest.java b/core/src/test/java/com/cloud/network/HAProxyConfiguratorTest.java
index 2a282cb..72361c2 100644
--- a/core/src/test/java/com/cloud/network/HAProxyConfiguratorTest.java
+++ b/core/src/test/java/com/cloud/network/HAProxyConfiguratorTest.java
@@ -31,6 +31,7 @@
import com.cloud.agent.api.routing.LoadBalancerConfigCommand;
import com.cloud.agent.api.to.LoadBalancerTO;
import com.cloud.network.lb.LoadBalancingRule.LbDestination;
+import com.cloud.network.lb.LoadBalancingRule.LbSslCert;
import java.util.List;
import java.util.ArrayList;
@@ -80,11 +81,11 @@
HAProxyConfigurator hpg = new HAProxyConfigurator();
LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false);
String result = genConfig(hpg, cmd);
- assertTrue("keepalive disabled should result in 'mode http' in the resulting haproxy config", result.contains("mode http"));
+ assertTrue("keepalive disabled should result in 'option httpclose' in the resulting haproxy config", result.contains("\toption httpclose"));
cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "4", true);
result = genConfig(hpg, cmd);
- assertTrue("keepalive enabled should not result in 'mode http' in the resulting haproxy config", !result.contains("mode http"));
+ assertTrue("keepalive enabled should result in 'no option httpclose' in the resulting haproxy config", result.contains("\tno option httpclose"));
// TODO
// create lb command
// setup tests for
@@ -122,6 +123,19 @@
Assert.assertTrue(result.contains("acl network_allowed src 1.1.1.1 2.2.2.2/24 \n\ttcp-request connection reject if !network_allowed"));
}
+ @Test
+ public void generateConfigurationTestWithSslCert() {
+ LoadBalancerTO lb = new LoadBalancerTO("1", "10.2.0.1", 443, "ssl", "roundrobin", false, false, false, null);
+ final LbSslCert lbSslCert = new LbSslCert("cert", "key", "password", "chain", "fingerprint", false);
+ lb.setLbSslCert(lbSslCert);
+ LoadBalancerTO[] lba = new LoadBalancerTO[1];
+ lba[0] = lb;
+ HAProxyConfigurator hpg = new HAProxyConfigurator();
+ LoadBalancerConfigCommand cmd = new LoadBalancerConfigCommand(lba, "10.0.0.1", "10.1.0.1", "10.1.1.1", null, 1L, "12", false);
+ String result = genConfig(hpg, cmd);
+ Assert.assertTrue(result.contains("bind 10.2.0.1:443 ssl crt /etc/cloudstack/ssl/10_2_0_1-443.pem"));
+ }
+
private String genConfig(HAProxyConfigurator hpg, LoadBalancerConfigCommand cmd) {
String[] sa = hpg.generateConfiguration(cmd);
StringBuilder sb = new StringBuilder();
diff --git a/engine/schema/src/main/java/com/cloud/network/dao/SslCertDao.java b/engine/schema/src/main/java/com/cloud/network/dao/SslCertDao.java
index 80bb44a..1e73cd7 100644
--- a/engine/schema/src/main/java/com/cloud/network/dao/SslCertDao.java
+++ b/engine/schema/src/main/java/com/cloud/network/dao/SslCertDao.java
@@ -22,4 +22,6 @@
public interface SslCertDao extends GenericDao<SslCertVO, Long> {
List<SslCertVO> listByAccountId(Long id);
+
+ int removeByAccountId(long accountId);
}
diff --git a/engine/schema/src/main/java/com/cloud/network/dao/SslCertDaoImpl.java b/engine/schema/src/main/java/com/cloud/network/dao/SslCertDaoImpl.java
index 185c18a..efadc00 100644
--- a/engine/schema/src/main/java/com/cloud/network/dao/SslCertDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/network/dao/SslCertDaoImpl.java
@@ -40,4 +40,10 @@
return listBy(sc);
}
+ @Override
+ public int removeByAccountId(long accountId) {
+ SearchCriteria<SslCertVO> sc = listByAccountId.create();
+ sc.setParameters("accountId", accountId);
+ return remove(sc);
+ }
}
diff --git a/server/src/main/java/com/cloud/network/element/VirtualRouterElement.java b/server/src/main/java/com/cloud/network/element/VirtualRouterElement.java
index 430d475..0978fcb 100644
--- a/server/src/main/java/com/cloud/network/element/VirtualRouterElement.java
+++ b/server/src/main/java/com/cloud/network/element/VirtualRouterElement.java
@@ -517,9 +517,11 @@
final Map<Capability, String> lbCapabilities = new HashMap<Capability, String>();
lbCapabilities.put(Capability.SupportedLBAlgorithms, "roundrobin,leastconn,source");
lbCapabilities.put(Capability.SupportedLBIsolation, "dedicated");
- lbCapabilities.put(Capability.SupportedProtocols, "tcp, udp, tcp-proxy");
+ lbCapabilities.put(Capability.SupportedProtocols, "tcp, udp, tcp-proxy, ssl");
lbCapabilities.put(Capability.SupportedStickinessMethods, getHAProxyStickinessCapability());
lbCapabilities.put(Capability.LbSchemes, LoadBalancerContainer.Scheme.Public.toString());
+ // Supports SSL offloading
+ lbCapabilities.put(Capability.SslTermination, "true");
// specifies that LB rules can support autoscaling and the list of
// counters it supports
diff --git a/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java b/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java
index ee4fe62..f786626 100644
--- a/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java
+++ b/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java
@@ -1267,10 +1267,10 @@
@Override
@DB
@ActionEvent(eventType = EventTypes.EVENT_LB_CERT_ASSIGN, eventDescription = "assigning certificate to load balancer", async = true)
- public boolean assignCertToLoadBalancer(long lbRuleId, Long certId) {
+ public boolean assignCertToLoadBalancer(long lbRuleId, Long certId, boolean forced) {
CallContext caller = CallContext.current();
- LoadBalancerVO loadBalancer = _lbDao.findById(Long.valueOf(lbRuleId));
+ LoadBalancerVO loadBalancer = _lbDao.findById(lbRuleId);
if (loadBalancer == null) {
throw new InvalidParameterValueException("Invalid load balancer id: " + lbRuleId);
}
@@ -1292,10 +1292,7 @@
throw new InvalidParameterValueException("Ssl termination not supported by the loadbalancer");
}
- //check if the lb is already bound
- LoadBalancerCertMapVO certMapRule = _lbCertMapDao.findByLbRuleId(loadBalancer.getId());
- if (certMapRule != null)
- throw new InvalidParameterValueException("Another certificate is already bound to the LB");
+ validateCertMapRule(lbRuleId, forced);
//check for correct port
if (loadBalancer.getLbProtocol() == null || !(loadBalancer.getLbProtocol().equals(NetUtils.SSL_PROTO)))
@@ -1326,6 +1323,18 @@
return success;
}
+ private void validateCertMapRule(long lbRuleId, boolean forced) {
+ //check if the lb is already bound
+ LoadBalancerCertMapVO certMapRule = _lbCertMapDao.findByLbRuleId(lbRuleId);
+ if (certMapRule != null) {
+ if (!forced) {
+ throw new InvalidParameterValueException("Another certificate is already bound to the LB");
+ }
+ logger.debug("Another certificate is already bound to the LB, removing it");
+ removeCertFromLoadBalancer(lbRuleId);
+ }
+ }
+
@Override
@DB
@ActionEvent(eventType = EventTypes.EVENT_LB_CERT_REMOVE, eventDescription = "removing certificate from load balancer", async = true)
@@ -1987,7 +1996,7 @@
return handled;
}
- private LoadBalancingRule getLoadBalancerRuleToApply(LoadBalancerVO lb) {
+ protected LoadBalancingRule getLoadBalancerRuleToApply(LoadBalancerVO lb) {
List<LbStickinessPolicy> policyList = getStickinessPolicies(lb.getId());
Ip sourceIp = getSourceIp(lb);
@@ -2257,12 +2266,17 @@
LoadBalancerVO tmplbVo = _lbDao.findById(lbRuleId);
boolean success = _lbDao.update(lbRuleId, lb);
- // If algorithm is changed, have to reapply the lb config
- if ((algorithm != null) && (tmplbVo.getAlgorithm().compareTo(algorithm) != 0)){
+ // If algorithm or lb protocol is changed, have to reapply the lb config
+ boolean needToReApplyRule = (algorithm != null && !algorithm.equals(tmplbVo.getAlgorithm()))
+ || (lbProtocol != null && !lbProtocol.equals(tmplbVo.getLbProtocol()));
+ if (needToReApplyRule) {
try {
lb.setState(FirewallRule.State.Add);
_lbDao.persist(lb);
applyLoadBalancerConfig(lbRuleId);
+ if (!lb.getLbProtocol().equals(NetUtils.SSL_PROTO)) {
+ removeCertMapIfExists(lb);
+ }
} catch (ResourceUnavailableException e) {
if (isRollBackAllowedForProvider(lb)) {
/*
@@ -2279,6 +2293,9 @@
if (lbBackup.getAlgorithm() != null) {
lb.setAlgorithm(lbBackup.getAlgorithm());
}
+ if (lbBackup.getLbProtocol() != null) {
+ lb.setLbProtocol(lbBackup.getLbProtocol());
+ }
lb.setState(lbBackup.getState());
_lbDao.update(lb.getId(), lb);
_lbDao.persist(lb);
@@ -2309,6 +2326,14 @@
}
}
+ private void removeCertMapIfExists(LoadBalancerVO lb) {
+ LoadBalancerCertMapVO loadBalancerCertMapVO = _lbCertMapDao.findByLbRuleId(lb.getId());
+ if (loadBalancerCertMapVO != null) {
+ logger.debug("Removing SSL cert for load balancer %s as the new protocol is not ssl but %s", lb, lb.getLbProtocol());
+ _lbCertMapDao.remove(loadBalancerCertMapVO.getId());
+ }
+ }
+
@Override
public Pair<List<? extends UserVm>, List<String>> listLoadBalancerInstances(ListLoadBalancerRuleInstancesCmd cmd) throws PermissionDeniedException {
Account caller = CallContext.current().getCallingAccount();
diff --git a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java
index 10da04d..278c253 100644
--- a/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java
+++ b/server/src/main/java/com/cloud/network/router/CommandSetupHelper.java
@@ -366,6 +366,7 @@
final LoadBalancerTO lb = new LoadBalancerTO(uuid, srcIp, srcPort, protocol, algorithm, revoked, false, inline, destinations, stickinessPolicies);
lb.setCidrList(rule.getCidrList());
lb.setLbProtocol(lb_protocol);
+ lb.setLbSslCert(rule.getLbSslCert());
lbs[i++] = lb;
}
String routerPublicIp = null;
diff --git a/server/src/main/java/com/cloud/network/router/NetworkHelperImpl.java b/server/src/main/java/com/cloud/network/router/NetworkHelperImpl.java
index f87e14c..d026bd9 100644
--- a/server/src/main/java/com/cloud/network/router/NetworkHelperImpl.java
+++ b/server/src/main/java/com/cloud/network/router/NetworkHelperImpl.java
@@ -929,6 +929,8 @@
return false;
}
+ validateHAproxyLbProtocol(rule.getLbProtocol());
+
for (final LoadBalancingRule.LbStickinessPolicy stickinessPolicy : rule.getStickinessPolicies()) {
final List<Pair<String, String>> paramsList = stickinessPolicy.getParams();
@@ -982,6 +984,13 @@
return true;
}
+ private void validateHAproxyLbProtocol(String lbProtocol) {
+ List<String> lbProtocols = Arrays.asList("tcp", "udp", "tcp-proxy", "ssl");
+ if (lbProtocol != null && !lbProtocols.contains(lbProtocol)) {
+ throw new InvalidParameterValueException(String.format("protocol %s is not in valid protocols %s", lbProtocol, lbProtocols));
+ }
+ }
+
/*
* This function detects numbers like 12 ,32h ,42m .. etc,. 1) plain number
* like 12 2) time or tablesize like 12h, 34m, 45k, 54m , here last
diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java
index 8e48612..19cec19 100644
--- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java
+++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java
@@ -1737,11 +1737,13 @@
.append(",sourcePortEnd=").append(firewallRuleVO.getSourcePortEnd());
if (firewallRuleVO instanceof LoadBalancerVO) {
LoadBalancerVO loadBalancerVO = (LoadBalancerVO) firewallRuleVO;
- loadBalancingData.append(",sourceIp=").append(_ipAddressDao.findById(loadBalancerVO.getSourceIpAddressId()).getAddress().toString())
+ String sourceIp = _ipAddressDao.findById(loadBalancerVO.getSourceIpAddressId()).getAddress().toString();
+ loadBalancingData.append(",sourceIp=").append(sourceIp)
.append(",destPortStart=").append(loadBalancerVO.getDefaultPortStart())
.append(",destPortEnd=").append(loadBalancerVO.getDefaultPortEnd())
.append(",algorithm=").append(loadBalancerVO.getAlgorithm())
.append(",protocol=").append(loadBalancerVO.getLbProtocol());
+ updateWithLbRuleSslCertificates(loadBalancingData, loadBalancerVO, sourceIp);
} else if (firewallRuleVO instanceof ApplicationLoadBalancerRuleVO) {
ApplicationLoadBalancerRuleVO appLoadBalancerVO = (ApplicationLoadBalancerRuleVO) firewallRuleVO;
loadBalancingData.append(",sourceIp=").append(appLoadBalancerVO.getSourceIp())
@@ -1760,6 +1762,16 @@
}
}
+ protected void updateWithLbRuleSslCertificates(final StringBuilder loadBalancingData, LoadBalancerVO loadBalancerVO, String sourceIp) {
+ if (NetUtils.SSL_PROTO.equals(loadBalancerVO.getLbProtocol())) {
+ final LbSslCert sslCert = _lbMgr.getLbSslCert(loadBalancerVO.getId());
+ if (sslCert != null && ! sslCert.isRevoked()) {
+ loadBalancingData.append(",sslcert=").append(sourceIp.replace(".", "_")).append('-')
+ .append(loadBalancerVO.getSourcePortStart()).append(".pem");
+ }
+ }
+ }
+
protected Map<String, String> getRouterHealthChecksConfig(final DomainRouterVO router) {
Map<String, String> data = new HashMap<>();
List<DomainRouterJoinVO> routerJoinVOs = domainRouterJoinDao.searchByIds(router.getId());
diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
index a73a00d..b5da605 100644
--- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java
+++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
@@ -136,6 +136,7 @@
import com.cloud.network.dao.NetworkVO;
import com.cloud.network.dao.RemoteAccessVpnDao;
import com.cloud.network.dao.RemoteAccessVpnVO;
+import com.cloud.network.dao.SslCertDao;
import com.cloud.network.dao.VpnUserDao;
import com.cloud.network.router.VirtualRouter;
import com.cloud.network.security.SecurityGroupManager;
@@ -309,6 +310,8 @@
private UserDataDao userDataDao;
@Inject
private NetworkPermissionDao networkPermissionDao;
+ @Inject
+ private SslCertDao sslCertDao;
private List<QuerySelector> _querySelectors;
@@ -1203,6 +1206,9 @@
// Delete registered UserData
userDataDao.removeByAccountId(accountId);
+ // Delete SSL certificates
+ sslCertDao.removeByAccountId(accountId);
+
// Delete Webhooks
deleteWebhooksForAccount(accountId);
diff --git a/server/src/main/java/org/apache/cloudstack/network/ssl/CertServiceImpl.java b/server/src/main/java/org/apache/cloudstack/network/ssl/CertServiceImpl.java
index 928e58a..d101ab9 100644
--- a/server/src/main/java/org/apache/cloudstack/network/ssl/CertServiceImpl.java
+++ b/server/src/main/java/org/apache/cloudstack/network/ssl/CertServiceImpl.java
@@ -26,8 +26,9 @@
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.CertPathBuilder;
import java.security.cert.CertPathBuilderException;
import java.security.cert.CertStore;
@@ -48,10 +49,6 @@
import java.util.List;
import java.util.Set;
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
import javax.inject.Inject;
import org.apache.cloudstack.acl.SecurityChecker;
@@ -62,9 +59,21 @@
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.network.tls.CertService;
import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openssl.PEMEncryptedKeyPair;
+import org.bouncycastle.openssl.PEMKeyPair;
+import org.bouncycastle.openssl.PEMParser;
+import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
+import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
+import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
+import org.bouncycastle.operator.InputDecryptorProvider;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
+import org.bouncycastle.pkcs.PKCSException;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
@@ -89,7 +98,6 @@
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.security.CertificateHelper;
import com.google.common.base.Preconditions;
-import org.apache.commons.lang3.StringUtils;
public class CertServiceImpl implements CertService {
@@ -279,11 +287,11 @@
return certResponseList;
}
- private void validate(final String certInput, final String keyInput, final String password, final String chainInput, boolean revocationEnabled) {
+ protected void validate(final String certInput, final String keyInput, final String password, final String chainInput, boolean revocationEnabled) {
try {
List<Certificate> chain = null;
final Certificate cert = parseCertificate(certInput);
- final PrivateKey key = parsePrivateKey(keyInput);
+ final PrivateKey key = parsePrivateKey(keyInput, password);
if (chainInput != null) {
chain = CertificateHelper.parseChain(chainInput);
@@ -295,7 +303,9 @@
if (chainInput != null) {
validateChain(chain, cert, revocationEnabled);
}
- } catch (final IOException | CertificateException e) {
+ } catch (final IOException | CertificateException | OperatorCreationException | PKCSException |
+ NoSuchAlgorithmException | InvalidKeySpecException e) {
+ logger.warn("Failed to validate certificate", e);
throw new IllegalStateException("Parsing certificate/key failed: " + e.getMessage(), e);
}
}
@@ -370,18 +380,17 @@
try {
final String data = "ENCRYPT_DATA";
- final SecureRandom random = new SecureRandom();
- final Cipher cipher = Cipher.getInstance(pubKey.getAlgorithm());
- cipher.init(Cipher.ENCRYPT_MODE, privKey, random);
- final byte[] encryptedData = cipher.doFinal(data.getBytes());
+ Signature sig = Signature.getInstance("SHA256withRSA");
+ sig.initSign(privKey);
+ sig.update(data.getBytes());
+ byte[] signature = sig.sign();
- cipher.init(Cipher.DECRYPT_MODE, pubKey, random);
- final String decreptedData = new String(cipher.doFinal(encryptedData));
- if (!decreptedData.equals(data)) {
+ sig.initVerify(pubKey);
+ sig.update(data.getBytes());
+ if (!sig.verify(signature)) {
throw new IllegalStateException("Bad public-private key");
}
-
- } catch (final BadPaddingException | IllegalBlockSizeException | InvalidKeyException | NoSuchPaddingException e) {
+ } catch (final InvalidKeyException | SignatureException e) {
throw new IllegalStateException("Bad public-private key", e);
} catch (final NoSuchAlgorithmException e) {
throw new IllegalStateException("Invalid algorithm for public-private key", e);
@@ -423,19 +432,55 @@
}
- public PrivateKey parsePrivateKey(final String key) throws IOException {
+ public PrivateKey parsePrivateKey(final String key, String password) throws IOException, OperatorCreationException, PKCSException, NoSuchAlgorithmException, InvalidKeySpecException {
Preconditions.checkArgument(StringUtils.isNotEmpty(key));
- try (final PemReader pemReader = new PemReader(new StringReader(key));) {
- final PemObject pemObject = pemReader.readPemObject();
- final byte[] content = pemObject.getContent();
- final PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(content);
- final KeyFactory factory = KeyFactory.getInstance("RSA", "BC");
- return factory.generatePrivate(privKeySpec);
- } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
- throw new IOException("No encryption provider available.", e);
- } catch (final InvalidKeySpecException e) {
- throw new IOException("Invalid Key format.", e);
+ PEMParser pemParser = new PEMParser(new StringReader(key));
+ Object privateKeyObj = pemParser.readObject();
+ if (privateKeyObj == null) {
+ throw new CloudRuntimeException("Cannot parse private key");
}
+ PrivateKey privateKey;
+ if (privateKeyObj instanceof PKCS8EncryptedPrivateKeyInfo) {
+ privateKey = parsePKCS8EncryptedPrivateKeyInfo((PKCS8EncryptedPrivateKeyInfo)privateKeyObj, password);
+ } else if (privateKeyObj instanceof PEMEncryptedKeyPair) {
+ privateKey = parsePEMEncryptedKeyPair((PEMEncryptedKeyPair)privateKeyObj, password);
+ } else if (privateKeyObj instanceof PEMKeyPair) {
+ // Key pair
+ PEMKeyPair pemKeyPair = (PEMKeyPair) privateKeyObj;
+ privateKey = new JcaPEMKeyConverter().getKeyPair(pemKeyPair).getPrivate();
+ } else if (privateKeyObj instanceof PrivateKeyInfo) {
+ // Private key only
+ PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) privateKeyObj;
+ privateKey = new JcaPEMKeyConverter().getPrivateKey(privateKeyInfo);
+ } else {
+ throw new IllegalArgumentException("Unsupported PEM object: " + privateKeyObj.getClass());
+ }
+ pemParser.close();
+ return privateKey;
+ }
+
+ private PrivateKey parsePKCS8EncryptedPrivateKeyInfo(PKCS8EncryptedPrivateKeyInfo privateKeyObj, String password)
+ throws IOException, OperatorCreationException, PKCSException, NoSuchAlgorithmException, InvalidKeySpecException {
+ if (password == null) {
+ throw new CloudRuntimeException("Key is encrypted by PKCS#8 but password is null");
+ }
+ PKCS8EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = (PKCS8EncryptedPrivateKeyInfo)privateKeyObj;
+ JceOpenSSLPKCS8DecryptorProviderBuilder builder = new JceOpenSSLPKCS8DecryptorProviderBuilder();
+ InputDecryptorProvider decryptor = builder.build(password.toCharArray());
+
+ PrivateKeyInfo privateKeyInfo = encryptedPrivateKeyInfo.decryptPrivateKeyInfo(decryptor);
+ String algorithm = privateKeyInfo.getPrivateKeyAlgorithm().getAlgorithm().getId();
+ KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyInfo.getEncoded());
+ return keyFactory.generatePrivate(keySpec);
+ }
+
+ private PrivateKey parsePEMEncryptedKeyPair(PEMEncryptedKeyPair encryptedKeyPair, String password) throws IOException {
+ if (password == null) {
+ throw new CloudRuntimeException("Key is encrypted but password is null");
+ }
+ return new JcaPEMKeyConverter().getKeyPair(
+ encryptedKeyPair.decryptKeyPair(new JcePEMDecryptorProviderBuilder().build(password.toCharArray()))).getPrivate();
}
@Override
diff --git a/server/src/test/java/com/cloud/network/lb/LoadBalancingRulesManagerImplTest.java b/server/src/test/java/com/cloud/network/lb/LoadBalancingRulesManagerImplTest.java
index 184c853..78655ba 100644
--- a/server/src/test/java/com/cloud/network/lb/LoadBalancingRulesManagerImplTest.java
+++ b/server/src/test/java/com/cloud/network/lb/LoadBalancingRulesManagerImplTest.java
@@ -17,12 +17,30 @@
package com.cloud.network.lb;
+import com.cloud.exception.ResourceUnavailableException;
import com.cloud.network.Network;
+import com.cloud.network.NetworkModel;
+import com.cloud.network.dao.LoadBalancerCertMapDao;
+import com.cloud.network.dao.LoadBalancerCertMapVO;
+import com.cloud.network.dao.LoadBalancerDao;
import com.cloud.network.dao.LoadBalancerVO;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO;
+import com.cloud.network.dao.SslCertVO;
+import com.cloud.offerings.dao.NetworkOfferingServiceMapDao;
+import com.cloud.user.Account;
+import com.cloud.user.AccountManager;
+import com.cloud.user.AccountVO;
+import com.cloud.user.User;
+import com.cloud.user.UserVO;
+import com.cloud.utils.db.EntityManager;
import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.utils.net.NetUtils;
+import org.apache.cloudstack.acl.SecurityChecker;
+import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.command.user.loadbalancer.UpdateLoadBalancerRuleCmd;
+import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.junit.Assert;
import org.junit.Test;
@@ -32,11 +50,16 @@
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.test.util.ReflectionTestUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.UUID;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
@@ -48,10 +71,39 @@
@Mock
NetworkOrchestrationService _networkMgr;
+ @Mock
+ LoadBalancerDao _lbDao;
+
+ @Mock
+ EntityManager _entityMgr;
+
+ @Mock
+ AccountManager _accountMgr;
+
+ @Mock
+ NetworkModel _networkModel;
+
+ @Mock
+ LoadBalancerCertMapDao _lbCertMapDao;
+
+ @Mock
+ NetworkOfferingServiceMapDao _networkOfferingServiceDao;
+
@Spy
@InjectMocks
LoadBalancingRulesManagerImpl lbr = new LoadBalancingRulesManagerImpl();
+ @Mock
+ NetworkVO networkMock;
+
+ @Mock
+ LoadBalancerVO loadBalancerMock;
+
+ private long accountId = 10L;
+ private long lbRuleId = 2L;
+ private long certMapRuleId = 3L;
+ private long networkId = 4L;
+
@Test
public void generateCidrStringTestNullCidrList() {
String result = lbr.generateCidrString(null);
@@ -83,7 +135,7 @@
List<Network.Provider> providers = Arrays.asList(Network.Provider.VirtualRouter);
when(loadBalancerMock.getNetworkId()).thenReturn(10L);
- when(_networkDao.findById(Mockito.anyLong())).thenReturn(networkMock);
+ when(_networkDao.findById(anyLong())).thenReturn(networkMock);
when(_networkMgr.getProvidersForServiceInNetwork(networkMock, Network.Service.Lb)).thenReturn(providers);
Network.Provider provider = lbr.getLoadBalancerServiceProvider(loadBalancerMock);
@@ -101,4 +153,159 @@
Network.Provider provider = lbr.getLoadBalancerServiceProvider(loadBalancerMock);
}
+
+ @Test
+ public void testAssignCertToLoadBalancer() throws Exception {
+ long accountId = 10L;
+ long lbRuleId = 2L;
+ long certId = 3L;
+ long networkId = 4L;
+
+ AccountVO account = new AccountVO("testaccount", 1L, "networkdomain", Account.Type.NORMAL, "uuid");
+ account.setId(accountId);
+ UserVO user = new UserVO(1, "testuser", "password", "firstname", "lastName", "email", "timezone",
+ UUID.randomUUID().toString(), User.Source.UNKNOWN);
+ CallContext.register(user, account);
+
+ LoadBalancerVO loadBalancerMock = Mockito.mock(LoadBalancerVO.class);
+ when(_lbDao.findById(lbRuleId)).thenReturn(loadBalancerMock);
+ when(loadBalancerMock.getId()).thenReturn(lbRuleId);
+ when(loadBalancerMock.getAccountId()).thenReturn(accountId);
+ when(loadBalancerMock.getNetworkId()).thenReturn(networkId);
+ when(loadBalancerMock.getLbProtocol()).thenReturn(NetUtils.SSL_PROTO);
+
+ SslCertVO certVO = Mockito.mock(SslCertVO.class);
+ when(_entityMgr.findById(SslCertVO.class, certId)).thenReturn(certVO);
+ when(certVO.getAccountId()).thenReturn(accountId);
+
+ LoadBalancerCertMapVO certMapRule = Mockito.mock(LoadBalancerCertMapVO.class);
+ when(_lbCertMapDao.findByLbRuleId(lbRuleId)).thenReturn(certMapRule);
+
+ Mockito.doNothing().when(_accountMgr).checkAccess(Mockito.any(Account.class), Mockito.isNull(SecurityChecker.AccessType.class), Mockito.eq(true), Mockito.any(LoadBalancerVO.class));
+
+ Mockito.doReturn("LB").when(lbr).getLBCapability(networkId, Network.Capability.SslTermination.getName());
+ Mockito.doReturn(true).when(lbr).applyLoadBalancerConfig(lbRuleId);
+
+ lbr.assignCertToLoadBalancer(lbRuleId, certId, true);
+
+ Mockito.verify(lbr, times(2)).applyLoadBalancerConfig(lbRuleId);
+ }
+
+ private void setupUpdateLoadBalancerRule() throws Exception{
+ AccountVO account = new AccountVO("testaccount", 1L, "networkdomain", Account.Type.NORMAL, "uuid");
+ account.setId(accountId);
+ UserVO user = new UserVO(1, "testuser", "password", "firstname", "lastName", "email", "timezone",
+ UUID.randomUUID().toString(), User.Source.UNKNOWN);
+ CallContext.register(user, account);
+
+ when(_lbDao.findById(lbRuleId)).thenReturn(loadBalancerMock);
+ when(loadBalancerMock.getId()).thenReturn(lbRuleId);
+ when(loadBalancerMock.getNetworkId()).thenReturn(networkId);
+
+ when(_networkDao.findById(networkId)).thenReturn(networkMock);
+
+ Mockito.doNothing().when(_accountMgr).checkAccess(Mockito.any(Account.class), Mockito.isNull(SecurityChecker.AccessType.class), Mockito.eq(true), Mockito.any(LoadBalancerVO.class));
+
+ LoadBalancingRule loadBalancingRule = Mockito.mock(LoadBalancingRule.class);
+ Mockito.doReturn(loadBalancingRule).when(lbr).getLoadBalancerRuleToApply(loadBalancerMock);
+ Mockito.doReturn(true).when(lbr).validateLbRule(loadBalancingRule);
+ Mockito.doReturn(true).when(lbr).applyLoadBalancerConfig(lbRuleId);
+
+ when(_lbDao.update(lbRuleId, loadBalancerMock)).thenReturn(true);
+
+ LoadBalancerCertMapVO certMapRule = Mockito.mock(LoadBalancerCertMapVO.class);
+ when(_lbCertMapDao.findByLbRuleId(lbRuleId)).thenReturn(certMapRule);
+ when(certMapRule.getId()).thenReturn(certMapRuleId);
+ }
+
+ @Test
+ public void testUpdateLoadBalancerRule1() throws Exception {
+ setupUpdateLoadBalancerRule();
+
+ // Update protocol from TCP to SSL
+ UpdateLoadBalancerRuleCmd cmd = new UpdateLoadBalancerRuleCmd();
+ ReflectionTestUtils.setField(cmd, ApiConstants.ID, lbRuleId);
+ ReflectionTestUtils.setField(cmd, "lbProtocol", NetUtils.SSL_PROTO);
+ when(loadBalancerMock.getLbProtocol()).thenReturn(NetUtils.TCP_PROTO).thenReturn(NetUtils.SSL_PROTO);
+
+ lbr.updateLoadBalancerRule(cmd);
+
+ Mockito.verify(lbr, times(1)).applyLoadBalancerConfig(lbRuleId);
+ Mockito.verify(_lbCertMapDao, never()).remove(anyLong());
+ }
+
+ @Test
+ public void testUpdateLoadBalancerRule2() throws Exception {
+ setupUpdateLoadBalancerRule();
+
+ // Update protocol from SSL to TCP
+ UpdateLoadBalancerRuleCmd cmd = new UpdateLoadBalancerRuleCmd();
+ ReflectionTestUtils.setField(cmd, ApiConstants.ID, lbRuleId);
+ ReflectionTestUtils.setField(cmd, "lbProtocol", NetUtils.TCP_PROTO);
+ when(loadBalancerMock.getLbProtocol()).thenReturn(NetUtils.SSL_PROTO).thenReturn(NetUtils.TCP_PROTO);
+
+ lbr.updateLoadBalancerRule(cmd);
+
+ Mockito.verify(_lbCertMapDao, times(1)).remove(anyLong());
+ Mockito.verify(lbr, times(1)).applyLoadBalancerConfig(lbRuleId);
+ }
+
+ @Test
+ public void testUpdateLoadBalancerRule3() throws Exception {
+ setupUpdateLoadBalancerRule();
+
+ // Update algorithm from source to roundrobin, lb protocol is same
+ UpdateLoadBalancerRuleCmd cmd = new UpdateLoadBalancerRuleCmd();
+ ReflectionTestUtils.setField(cmd, ApiConstants.ID, lbRuleId);
+ ReflectionTestUtils.setField(cmd, "algorithm", "roundrobin");
+ ReflectionTestUtils.setField(cmd, "lbProtocol", NetUtils.SSL_PROTO);
+ when(loadBalancerMock.getAlgorithm()).thenReturn("source");
+ when(loadBalancerMock.getLbProtocol()).thenReturn(NetUtils.SSL_PROTO);
+
+ lbr.updateLoadBalancerRule(cmd);
+
+ Mockito.verify(lbr, times(1)).applyLoadBalancerConfig(lbRuleId);
+ Mockito.verify(_lbCertMapDao, never()).remove(anyLong());
+ }
+
+ @Test
+ public void testUpdateLoadBalancerRule4() throws Exception {
+ setupUpdateLoadBalancerRule();
+
+ // Update with same algorithm and protocol
+ UpdateLoadBalancerRuleCmd cmd = new UpdateLoadBalancerRuleCmd();
+ ReflectionTestUtils.setField(cmd, ApiConstants.ID, lbRuleId);
+ ReflectionTestUtils.setField(cmd, "algorithm", "roundrobin");
+ ReflectionTestUtils.setField(cmd, "lbProtocol", NetUtils.SSL_PROTO);
+ when(loadBalancerMock.getAlgorithm()).thenReturn("roundrobin");
+ when(loadBalancerMock.getLbProtocol()).thenReturn(NetUtils.SSL_PROTO);
+
+ lbr.updateLoadBalancerRule(cmd);
+
+ Mockito.verify(lbr, never()).applyLoadBalancerConfig(lbRuleId);
+ Mockito.verify(_lbCertMapDao, never()).remove(anyLong());
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testUpdateLoadBalancerRule5() throws Exception {
+ setupUpdateLoadBalancerRule();
+
+ // Update protocol from SSL to TCP, throws an exception
+ UpdateLoadBalancerRuleCmd cmd = new UpdateLoadBalancerRuleCmd();
+ ReflectionTestUtils.setField(cmd, ApiConstants.ID, lbRuleId);
+ ReflectionTestUtils.setField(cmd, "lbProtocol", NetUtils.TCP_PROTO);
+ when(loadBalancerMock.getLbProtocol()).thenReturn(NetUtils.SSL_PROTO).thenReturn(NetUtils.TCP_PROTO);
+ Mockito.doThrow(ResourceUnavailableException.class).when(lbr).applyLoadBalancerConfig(lbRuleId);
+
+ List<Network.Provider> providers = Arrays.asList(Network.Provider.VirtualRouter);
+ when(_networkDao.findById(anyLong())).thenReturn(networkMock);
+ when(_networkMgr.getProvidersForServiceInNetwork(networkMock, Network.Service.Lb)).thenReturn(providers);
+
+ lbr.updateLoadBalancerRule(cmd);
+
+ Mockito.verify(_lbCertMapDao, never()).remove(anyLong());
+ Mockito.verify(lbr, times(1)).applyLoadBalancerConfig(lbRuleId);
+ Mockito.verify(loadBalancerMock, times(1)).setLbProtocol(NetUtils.TCP_PROTO);
+ Mockito.verify(loadBalancerMock, times(1)).setLbProtocol(NetUtils.SSL_PROTO);
+ }
}
diff --git a/server/src/test/java/com/cloud/network/router/VirtualNetworkApplianceManagerImplTest.java b/server/src/test/java/com/cloud/network/router/VirtualNetworkApplianceManagerImplTest.java
index cadbc3e..0365ae2 100644
--- a/server/src/test/java/com/cloud/network/router/VirtualNetworkApplianceManagerImplTest.java
+++ b/server/src/test/java/com/cloud/network/router/VirtualNetworkApplianceManagerImplTest.java
@@ -40,6 +40,7 @@
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.LoadBalancerDao;
import com.cloud.network.dao.LoadBalancerVMMapDao;
+import com.cloud.network.dao.LoadBalancerVO;
import com.cloud.network.dao.MonitoringServiceDao;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO;
@@ -54,6 +55,8 @@
import com.cloud.network.dao.UserIpv6AddressDao;
import com.cloud.network.dao.VirtualRouterProviderDao;
import com.cloud.network.dao.VpnUserDao;
+import com.cloud.network.lb.LoadBalancingRule;
+import com.cloud.network.lb.LoadBalancingRulesManager;
import com.cloud.network.rules.dao.PortForwardingRulesDao;
import com.cloud.network.vpc.VpcVO;
import com.cloud.network.vpc.dao.VpcDao;
@@ -67,6 +70,7 @@
import com.cloud.user.dao.UserDao;
import com.cloud.user.dao.UserStatisticsDao;
import com.cloud.user.dao.UserStatsLogDao;
+import com.cloud.utils.net.NetUtils;
import com.cloud.vm.DomainRouterVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager;
@@ -259,6 +263,9 @@
@Mock
private BGPService bgpService;
+ @Mock
+ private LoadBalancingRulesManager _lbMgr;
+
// @InjectMocks
// private VirtualNetworkApplianceManagerImpl virtualNetworkApplianceManagerImpl;
@@ -391,4 +398,21 @@
Mockito.verify(_commandSetupHelper).createBgpPeersCommands(bgpPeers, router, cmds, network);
}
+
+ @Test
+ public void testUpdateWithLbRuleSslCertificates() {
+ StringBuilder loadBalancingData = new StringBuilder();
+ LoadBalancerVO loadBalancer = Mockito.mock(LoadBalancerVO.class);
+ when(loadBalancer.getLbProtocol()).thenReturn(NetUtils.SSL_PROTO);
+ when(loadBalancer.getId()).thenReturn(1L);
+ when(loadBalancer.getSourcePortStart()).thenReturn(443);
+ LoadBalancingRule.LbSslCert lbSslCert = Mockito.mock(LoadBalancingRule.LbSslCert.class);
+ when(lbSslCert.isRevoked()).thenReturn(false);
+ when(_lbMgr.getLbSslCert(1L)).thenReturn(lbSslCert);
+ String sourceIp = "1.2.3.4";
+
+ virtualNetworkApplianceManagerImpl.updateWithLbRuleSslCertificates(loadBalancingData, loadBalancer, sourceIp);
+
+ Assert.assertEquals(",sslcert=1_2_3_4-443.pem", loadBalancingData.toString());
+ }
}
diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java
index 3055e48..846d8cd 100644
--- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java
+++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java
@@ -201,6 +201,7 @@
Mockito.when(_sshKeyPairDao.listKeyPairs(Mockito.anyLong(), Mockito.anyLong())).thenReturn(sshkeyList);
Mockito.when(_sshKeyPairDao.remove(Mockito.anyLong())).thenReturn(true);
Mockito.when(userDataDao.removeByAccountId(Mockito.anyLong())).thenReturn(222);
+ Mockito.when(sslCertDao.removeByAccountId(Mockito.anyLong())).thenReturn(333);
Mockito.doNothing().when(accountManagerImpl).deleteWebhooksForAccount(Mockito.anyLong());
Mockito.doNothing().when(accountManagerImpl).verifyCallerPrivilegeForUserOrAccountOperations((Account) any());
diff --git a/server/src/test/java/com/cloud/user/AccountManagetImplTestBase.java b/server/src/test/java/com/cloud/user/AccountManagetImplTestBase.java
index 98f1520..90e2779 100644
--- a/server/src/test/java/com/cloud/user/AccountManagetImplTestBase.java
+++ b/server/src/test/java/com/cloud/user/AccountManagetImplTestBase.java
@@ -29,6 +29,7 @@
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.RemoteAccessVpnDao;
+import com.cloud.network.dao.SslCertDao;
import com.cloud.network.dao.VpnUserDao;
import com.cloud.network.security.SecurityGroupManager;
import com.cloud.network.security.dao.SecurityGroupDao;
@@ -198,6 +199,8 @@
@Mock
UserDataDao userDataDao;
@Mock
+ SslCertDao sslCertDao;
+ @Mock
NetworkPermissionDao networkPermissionDaoMock;
@Spy
diff --git a/server/src/test/java/org/apache/cloudstack/network/ssl/CertServiceTest.java b/server/src/test/java/org/apache/cloudstack/network/ssl/CertServiceTest.java
index 5a2f12f..0685167 100644
--- a/server/src/test/java/org/apache/cloudstack/network/ssl/CertServiceTest.java
+++ b/server/src/test/java/org/apache/cloudstack/network/ssl/CertServiceTest.java
@@ -34,6 +34,13 @@
import org.apache.cloudstack.api.command.user.loadbalancer.DeleteSslCertCmd;
import org.apache.cloudstack.api.command.user.loadbalancer.UploadSslCertCmd;
import org.apache.cloudstack.context.CallContext;
+import org.bouncycastle.openssl.PKCS8Generator;
+import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
+import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.OutputEncryptor;
+import org.bouncycastle.util.io.pem.PemObject;
+import org.bouncycastle.util.io.pem.PemWriter;
import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
@@ -44,9 +51,13 @@
import java.io.File;
import java.io.IOException;
+import java.io.StringWriter;
import java.lang.reflect.Field;
import java.net.URLDecoder;
import java.nio.charset.Charset;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@@ -207,7 +218,7 @@
}
}
- // @Test
+ @Test
/**
* Given a Self-signed Certificate with encrypted key, upload should succeed
*/
@@ -456,7 +467,7 @@
Assert.fail("Given an encrypted private key with a bad password. Upload should fail.");
} catch (final Exception e) {
Assert.assertTrue("Did not expect message: " + e.getMessage(),
- e.getMessage().contains("Parsing certificate/key failed: Invalid Key format."));
+ e.getMessage().contains("Parsing certificate/key failed: exception using cipher - please check password and data."));
}
}
@@ -544,7 +555,7 @@
Assert.fail("Given a private key which has a different algorithm than the certificate, upload should fail");
} catch (final Exception e) {
Assert.assertTrue("Did not expect message: " + e.getMessage(),
- e.getMessage().contains("Parsing certificate/key failed: Invalid Key format."));
+ e.getMessage().contains("Public and private key have different algorithms"));
}
}
@@ -821,4 +832,283 @@
return 1;
}
}
+
+ private String generateEncryptedPrivateKey(String password) throws NoSuchAlgorithmException, OperatorCreationException, IOException {
+ // Generate RSA key pair
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+ kpg.initialize(2048);
+ KeyPair keyPair = kpg.generateKeyPair();
+
+ // Build encryptor (AES-256-CBC is FIPS-approved)
+ OutputEncryptor encryptor = new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.AES_256_CBC)
+ .setPassword(password.toCharArray())
+ .build();
+
+ // Wrap the private key into PKCS#8 format and encrypt
+ JcaPKCS8Generator gen = new JcaPKCS8Generator(keyPair.getPrivate(), encryptor);
+ PemObject pemObject = gen.generate();
+
+ StringWriter stringWriter = new StringWriter();
+ try (PemWriter pemWriter = new PemWriter(stringWriter)) {
+ pemWriter.writeObject(pemObject);
+ }
+ return stringWriter.toString();
+ }
+
+ @Test
+ public void parseEncryptedPrivateKey() throws Exception{
+ String password = "strongpassword";
+ String key = generateEncryptedPrivateKey(password);
+ final CertServiceImpl certService = new CertServiceImpl();
+ certService.parsePrivateKey(key, password);
+ }
+
+ @Test
+ public void validateCertAndChainsWithEncryptedKey() {
+ String password = "strongpassword";
+ String key = "-----BEGIN ENCRYPTED PRIVATE KEY-----\n" +
+ "MIIFGzBVBgkqhkiG9w0BBQ0wSDAnBgkqhkiG9w0BBQwwGgQUiQiFcfHTx8EKYNHJ\n" +
+ "zOqT8/9AkaQCAggAMB0GCWCGSAFlAwQBKgQQKXBglXgHYSWK20BxSFUVLQSCBMBr\n" +
+ "ro2dXjsEoZfglccP5YWRPETSXntMdjAd39ftiWSXwQWZmht9/t+hSK+qZnGX/8VI\n" +
+ "0OR7x+8SBDqZAb9mYZzPPcUd/k+KLpQAFBSFrWVle40MY1OyZqEdQe3ELDERS919\n" +
+ "WRGmjTYUomL1zCAIrx27Woq5iiZkqsXmCcQwKRkCSNbTXjDe6gXtO9ePuMgvSiGg\n" +
+ "q2rhBZv82AYoc/IHzftsoS53Sda96RE93MK12+L48E5gxbqeHUJeGhn1hxxkqFcj\n" +
+ "cL/z817M6a9BEJkNlS4sZk3+Fg1RYBTx7CKYzR8WAf+LvasdO5ijPrNcqc6DzIIn\n" +
+ "tL0Kj/Gjp6rFP83IfezCtVdYi/dRLR9dNROJt7aIaeXnYdYF8o+vmWZm5H4bZeun\n" +
+ "czadKzd4EfvatHXi7Zq/cV/mh/NitUfnYMR5LUnX9pjNRkr2uqYx5AiO6aPQoR9G\n" +
+ "Gv1ubkUtug/rDoywwol7XGWxnDNbB4fvXRIGsyZYDh9J1CX+sv693ZeRx1J48vhT\n" +
+ "s+gZug8oG5DfSLCVaJDuIyHQGKuRLnh6LawUkyCA7Q/9vgmXnXo+0hJ5dYQw21fj\n" +
+ "M5yHrOt/tway5tJgDwuD778r3Y4w1H9Yt42J3tZL3gOIOyYhHad2M/emh5Khh/m/\n" +
+ "VK8eM86OQeo/zp+RddM4ckaUxKe/bFBqj9KvhzHsFTAuirT7be3+Ye1iBqKLvCgO\n" +
+ "yOTY14J1NbrvGmUs6yq3JxTkzl4+A23SPlHQE16j3UzCz0qnNTYLruUibL940uXu\n" +
+ "rnBcOuW6uM4yc+X3Aqo7xL3kzW/9waCd/VG/btJLNPKSDDRuuKQ7NEPjS+xajmqh\n" +
+ "WVMzzcMj4wVvTz6vnZNm9u9Yu/ACpzHTD+hVeFZhIscVCdT+LncEumLHHhrLQS3h\n" +
+ "9gVlv0MvSrWH6sl3oQEnA5ceEI4LfH6eT++IGAdKJTqkpAwSEtSEV+P/dETRNnsH\n" +
+ "TsKNEdylH++9Ljhkt68971cLGHf9yuzVU75BPFybngcNFZu3+YUDWY0fBwqwE0OI\n" +
+ "FXeqPhnN2UfAoeqCwz2KtPf2ig0a34S6Rxne9/XewlCsKEGSrdYG8mm4eJzsP69/\n" +
+ "5qw1MDO1nvt0B5jSly3vHcHGvgiDtG+vsfGqC1TA8eaTSq/UkUAKfoGg1DkL8olz\n" +
+ "b7jB24748Oh87Ksz12yeyY5T1edpoDcScCRLwIb0vNMKqIUe1aCEdTl08UHV3CbG\n" +
+ "7rnRLWE+9/Csij2fpkx0mEDeXdLxeSvkw5K8ha26s52MR4WhW0EUN74FJOMrTej3\n" +
+ "0jtcTC/bThc5jmQDaSQJbaiSIEKl8sdA0u8oTzBD2B1F9gkrZNZpE7hz670tysQs\n" +
+ "2Z0AxDcxQ7Qfkytg52MfJvLf0jxuNqjfbmQqkQsT+yUkjT6AmOgUMGP4zojP8ErY\n" +
+ "AvAqgurefHMS/HA8BUT7qxt300cTYaAONUlAJ/qAJ/YoHOI5yqWzBFJsr95NC13t\n" +
+ "rGqiOOLGtSIxk4WwdUX0u9TW8Hk6pWnl6MkyAn+a3RqKfrJ2tfKMjsO3iqu3Dlvz\n" +
+ "72RD5LsGcnhfKQ/TdswEA1EKdHBBjnDQOGdWNNTXnn41XoNNKneFjlFgJc8AXyoN\n" +
+ "fHvkc2aKb86WdpcANxK3\n" +
+ "-----END ENCRYPTED PRIVATE KEY-----";
+ String certificate = "-----BEGIN CERTIFICATE-----\n" +
+ "MIIERTCCAi0CFF2Ro8QjYcOCALfEqL1zs2T0cQzyMA0GCSqGSIb3DQEBCwUAMF4x\n" +
+ "CzAJBgNVBAYTAlhYMQswCQYDVQQIDAJYWDELMAkGA1UEBwwCWFgxEzARBgNVBAoM\n" +
+ "CkNsb3VkU3RhY2sxDzANBgNVBAsMBkFwYWNoZTEPMA0GA1UEAwwGQXBhY2hlMCAX\n" +
+ "DTI1MDYxNzA3MzQxOFoYDzIxMjUwNTI0MDczNDE4WjBeMQswCQYDVQQGEwJYWDEL\n" +
+ "MAkGA1UECAwCWFgxCzAJBgNVBAcMAlhYMRMwEQYDVQQKDApDbG91ZFN0YWNrMQ8w\n" +
+ "DQYDVQQLDAZBcGFjaGUxDzANBgNVBAMMBkFwYWNoZTCCASIwDQYJKoZIhvcNAQEB\n" +
+ "BQADggEPADCCAQoCggEBAMXpfAyO1m+YolspmNL64cMJ0mW4QiJUrrNxYyIaakfW\n" +
+ "/qs78hMlf8V82T94ayoMs2fpkjf69QsXTZoOZoUkaz58Wz9Z860OMAD/wguGz7EX\n" +
+ "Bk+OTEDhXP9NAkY99TqscWS3bm6XSu3w0cOwjwLtV72VsT2UA1d0hpVI4kVTbI56\n" +
+ "RZ1ymboyu/mhp2dqZu+Ewh8n7PMYvDO6hGuqsM5We2WLdSCmPZKtmbQ8CRj0fwJI\n" +
+ "CZZEafFEBwLhW3F15SRZLxQApzqMTlmbk9edEgOfJZqMrr+F8jguce7Qry6FcbkU\n" +
+ "6x4oRyykuz5pi5mPjaTxQyY4NWsCHojlQ0kz0VeBUX0CAwEAATANBgkqhkiG9w0B\n" +
+ "AQsFAAOCAgEAJAUldK70IoyA0jokXfAyLXNRX43/UfmQMu3xvVYI9OPk8f6CrBIm\n" +
+ "g79cA3pGPNxyIReqFxDk+wXW+/iPCgOwv+YYODPEMZi1Cc8WQJ4OGzovD5hep7TA\n" +
+ "pg6jo16LdKpOQM6C9XUce3vZf6t487PCgg8SzldqhMMC97Kw+DAxYg+JRd28jfIB\n" +
+ "RAtpOCzqKqWp7lQ1YwS9M/VI0mYtmiuQbaz1to4qBPcCbR1GsLsmqMmTUkbYYyFF\n" +
+ "fgvInITyW+0NV/UwgiNFxU+k9T2H1lfvqj6hVRwwj7i84xAu4Y/N9zP/UKXxU93N\n" +
+ "ogoHabfGcsFEygyTkFuI4XG/Ppc3c8CJV2NbVQixe5Wdt1Yc9qMkbq+OdGvsOhbt\n" +
+ "T2+Qz5JZ7w0LsYONzuCRbaDpJiAg2MiALe3L1RzEya57/PylgUeH6gMbPyuQ2EyL\n" +
+ "pTUQ1imV3tTlkxjy7niu/IeqgcQOA2cx8Fwok+ECLvxc47noUlgPcROz5i43+IYA\n" +
+ "frvGqDfZCeKXKuAi//8wBl2tptMMmLpkS4mW/8Pijcx3JuxC6ySeOFAVgPjq4krw\n" +
+ "dGl+IBNwKNcsUu5/3uj/2h85w56Ys8uxeLkLqEq+9yHlwxexGJG0qJ2QcXFnOxCC\n" +
+ "qz+L2k3m0+Yu5zUFsMCTgEwQeR6CUfW9/GtPunZtvwHOSbVus0DvnSE=\n" +
+ "-----END CERTIFICATE-----";
+ String certChains = "-----BEGIN CERTIFICATE-----\n" +
+ "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\n" +
+ "JZr9MEn1+w==\n" +
+ "-----END CERTIFICATE-----\n" +
+ "-----BEGIN CERTIFICATE-----\n" +
+ "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\n" +
+ "dwa9\n" +
+ "-----END CERTIFICATE-----";
+ final CertServiceImpl certService = new CertServiceImpl();
+ certService.validate(certificate, key, password, certChains, false);
+ }
+
+ @Test
+ public void validateCertAndChainsWithUnencryptedKey() {
+ String key = "-----BEGIN PRIVATE KEY-----\n" +
+ "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCph7jsoMCQirRn\n" +
+ "3obuvgnnefTXRQYd9tF9k2aCVkTiiisvC39px7MGdgvDXADhD9fmR7oyXVQlfNu0\n" +
+ "rXjjgsVT3r4bv+DVi81YGXnuU7h10yCOZJt21i6QGHN1CS0/TAfg0UhlACCEYNRx\n" +
+ "kB0klwUcj/jk/AKil1DoUGpvAm2gZsek/njb76/AeIfxc+Es4ZOPCVqQOHp6gI0q\n" +
+ "t6KDMkUwv8fyzrpScygMUPVYrLmm6D0pn8yd3ihW07wGxMjND6UgOnao8t6H3LaM\n" +
+ "Pe7eqSFzxunF9NFFjnUrKcHZZSledDM/37Kbqb/8T5f+4SwjioS1OdPCh8ApdiXq\n" +
+ "HNUwYkALAgMBAAECggEAK5JiiQ7X7053B6s96uaVDRVfRGTNKa5iMXBNDHq3wbHZ\n" +
+ "X4IJAVr+PE7ivxdKco3r45fT11X9ZpUsssdTJsZZiTDak69BTiFcaaRCnmqOIlpd\n" +
+ "J7vb6TMrTIW8RvxQ0M/txm6DuNHLibqJX5a2pszZ13l5cwECfF9/v/XLJTTukCbu\n" +
+ "6D/f3fBVFl1tM8y9saOEYLkdb4dILWY61bVSDNswgprz2EV1SFnk5jxz2FuBrM/Q\n" +
+ "+7hINvjDcaRvcm59hRb1rkljv7S10VoNw/CFkU451csJkUe4vWZwB8lZK/XxLQG0\n" +
+ "HEdS1zU1XY8H8Y1RCrxjGRyiiWsBtUThhWYlPrGCoQKBgQDkP09YAlKqXhT69Kx5\n" +
+ "keg2i1jV2hA73zWbWXt9xp5jG5r3pl3m170DvKL93YIDnHtpTC56mlzGrzS7DSTN\n" +
+ "p0buY9Qb3fkJxunCpPVFo0HMFkpeR77ax0v34NzSohlRLKFo5R2M1cmDfbVbnSSl\n" +
+ "MB57FfRRMxzjrk+dJvjOeJsxjwKBgQC+JLb4B8CZjpurXYg3ySiRqFsCqkqob+kf\n" +
+ "9dR+rWvcR6vMTEyha0hUlDvTikDepU2smYR4oPHfdcXF9lAJ7T02UmQDeizAqR68\n" +
+ "u9e+yS0q3tdRnPPZmXJfaDCXG1hKMqF4YA5Vs0XAjleF3zHB+vBLrnlPpShtd/Mu\n" +
+ "sWTpxICTxQKBgQDSr/n+pE5IQwYczOO0aFGwn5pF9L9NdPHXz5aleETV+TJn7WL6\n" +
+ "ZiRsoaDWs7SCvtxQS2kP9RM0t5/2FeDmEMXx4aZ2fsSWGM3IxVo+iL+Aswa81n8/\n" +
+ "Ff5y9lb/+29hNdBcsjk/ukwEG3Lf+UNNVAie15oppgPByzJkPwgmFsAy0wKBgHDX\n" +
+ "/TZp82WuerhSw/rHiSoYjhqg0bnw4Ju1Gy0q4q5SYqTWS0wpDT4U0wSSMjlwRQ6/\n" +
+ "9RxZ9/G0RXFc4tdhUkig0PY3VcPpGnLL0BhL8GBW69ZlnVpwdK4meV/UPKucLLPx\n" +
+ "3dACmszSLSMn+LG0qVNg8mHQFJQS8eGuKcOKePw5AoGACuxtefROKdKOALh4lTi2\n" +
+ "VOwPZ+1jxsm6lKNccIEvbUpe3UXPgNWpJiDX8mUcob4/NBLzmV3BUVKbG7Exbo5J\n" +
+ "LoMfp7OsztWUFwt7YAvRfS8fHdhkEsxEf3T72ADieH5ZAuXFF+K0H3r6HtWPD4ws\n" +
+ "mTJjGP4+Bl/dFakA5FJcjHg=\n" +
+ "-----END PRIVATE KEY-----";
+ String certificate = "-----BEGIN CERTIFICATE-----\n" +
+ "MIIERTCCAi0CFF2Ro8QjYcOCALfEqL1zs2T0cQzzMA0GCSqGSIb3DQEBCwUAMF4x\n" +
+ "CzAJBgNVBAYTAlhYMQswCQYDVQQIDAJYWDELMAkGA1UEBwwCWFgxEzARBgNVBAoM\n" +
+ "CkNsb3VkU3RhY2sxDzANBgNVBAsMBkFwYWNoZTEPMA0GA1UEAwwGQXBhY2hlMCAX\n" +
+ "DTI1MDYxNzA5MTE0N1oYDzIxMjUwNTI0MDkxMTQ3WjBeMQswCQYDVQQGEwJYWDEL\n" +
+ "MAkGA1UECAwCWFgxCzAJBgNVBAcMAlhYMRMwEQYDVQQKDApDbG91ZFN0YWNrMQ8w\n" +
+ "DQYDVQQLDAZBcGFjaGUxDzANBgNVBAMMBkFwYWNoZTCCASIwDQYJKoZIhvcNAQEB\n" +
+ "BQADggEPADCCAQoCggEBAKmHuOygwJCKtGfehu6+Ced59NdFBh320X2TZoJWROKK\n" +
+ "Ky8Lf2nHswZ2C8NcAOEP1+ZHujJdVCV827SteOOCxVPevhu/4NWLzVgZee5TuHXT\n" +
+ "II5km3bWLpAYc3UJLT9MB+DRSGUAIIRg1HGQHSSXBRyP+OT8AqKXUOhQam8CbaBm\n" +
+ "x6T+eNvvr8B4h/Fz4Szhk48JWpA4enqAjSq3ooMyRTC/x/LOulJzKAxQ9Visuabo\n" +
+ "PSmfzJ3eKFbTvAbEyM0PpSA6dqjy3ofctow97t6pIXPG6cX00UWOdSspwdllKV50\n" +
+ "Mz/fspupv/xPl/7hLCOKhLU508KHwCl2Jeoc1TBiQAsCAwEAATANBgkqhkiG9w0B\n" +
+ "AQsFAAOCAgEAOKaT7cp1P/B67cT0pQ+ZO7dazoomvwbznpUDPlX+h2f9pPYvBoOJ\n" +
+ "qul0Np3zft3sR4M1uxRNuayhd+oFMNx0J3CJVxc6fpUvc0IvNAgy0C6IeAlTTH6V\n" +
+ "Tiy8X5YeD1SAg0wJkqZQzXC+8Ao+LPacdhnz7wUSV1j4ILlVZcfvISaaZUFidERT\n" +
+ "nP18syUWSodTULXTKB8M8z/9t6KFWXJDJGXLKBMoX3DCSx9QG5GDMuyu9XWf3bBH\n" +
+ "ZHZse02mh0x83hV34Bpa1Yr98PsGvQm7GUXiLenFO57wzWaInxBkS6sF4OWreiMI\n" +
+ "lN94CtBXtMxtC5C50WthNGBJHg3dXKeF3O6F8z8EkkqpKyJtJ3IoAXTHGEh5fxp0\n" +
+ "tsbOEqJ540XbtD82UWYA4bVY1h0Tb1SaV7fylZkuYXZ+rl6G0S7roPVYbrjRsP9t\n" +
+ "FCGko35WkhkI0OpNoTremH+H1U/nBowMm6tSfZ0ZWa/4NnLacXhPjDJkEhu7RlA4\n" +
+ "JYeYKe4dj4hLdcHCUFuP8Tdv1P20SGQQOaHUXYbHP5Er3EHZxzI13JwHiO+FKuYP\n" +
+ "igIqbCdBd8smTzdbit0f6OfKOyNXDDxN+E1VKAHSquYuxMcj+njKTQ1ihpXnTLpo\n" +
+ "ZP3NoLZ6gAQIjEgHHsLeZ24HCbiFfUpwWSPNNcr6X5qQelt5leNGsIU=\n" +
+ "-----END CERTIFICATE-----";
+ String certChains = "-----BEGIN CERTIFICATE-----\n" +
+ "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\n" +
+ "JZr9MEn1+w==\n" +
+ "-----END CERTIFICATE-----\n" +
+ "-----BEGIN CERTIFICATE-----\n" +
+ "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\n" +
+ "dwa9\n" +
+ "-----END CERTIFICATE-----";
+ final CertServiceImpl certService = new CertServiceImpl();
+ certService.validate(certificate, key, null, certChains, false);
+ }
}
diff --git a/systemvm/debian/opt/cloud/bin/cs/CsLoadBalancer.py b/systemvm/debian/opt/cloud/bin/cs/CsLoadBalancer.py
index a92f06b..39de9b5 100755
--- a/systemvm/debian/opt/cloud/bin/cs/CsLoadBalancer.py
+++ b/systemvm/debian/opt/cloud/bin/cs/CsLoadBalancer.py
@@ -16,6 +16,7 @@
# under the License.
import logging
import os.path
+from os import listdir
import re
from cs.CsDatabag import CsDataBag
from .CsProcess import CsProcess
@@ -25,6 +26,7 @@
HAPROXY_CONF_T = "/etc/haproxy/haproxy.cfg.new"
HAPROXY_CONF_P = "/etc/haproxy/haproxy.cfg"
+SSL_CERTS_DIR = "/etc/cloudstack/ssl/"
class CsLoadBalancer(CsDataBag):
""" Manage Load Balancer entries """
@@ -34,6 +36,9 @@
return
if 'configuration' not in list(self.dbag['config'][0].keys()):
return
+ if 'ssl_certs' in list(self.dbag['config'][0].keys()):
+ self._create_pem_for_sslcert(self.dbag['config'][0]['ssl_certs'])
+
config = self.dbag['config'][0]['configuration']
file1 = CsFile(HAPROXY_CONF_T)
file1.empty()
@@ -43,6 +48,11 @@
file1.commit()
file2 = CsFile(HAPROXY_CONF_P)
if not file2.compare(file1):
+ # Verify new haproxy config before haproxy restart/reload
+ haproxy_err = self._verify_haproxy_config(HAPROXY_CONF_T)
+ if haproxy_err:
+ raise Exception("haproxy config is invalid with error \n%s" % haproxy_err)
+
CsHelper.copy(HAPROXY_CONF_T, HAPROXY_CONF_P)
proc = CsProcess(['/run/haproxy.pid'])
@@ -82,3 +92,29 @@
ip = path[0]
port = path[1]
firewall.append(["filter", "", "-A INPUT -p tcp -m tcp -d %s --dport %s -m state --state NEW -j ACCEPT" % (ip, port)])
+
+ def _create_pem_for_sslcert(self, ssl_certs):
+ logging.debug("CsLoadBalancer:: creating new pem files in %s and cleaning up it" % SSL_CERTS_DIR)
+ if not os.path.exists(SSL_CERTS_DIR):
+ CsHelper.execute("mkdir -p %s" % SSL_CERTS_DIR)
+ cert_names = []
+ for cert in ssl_certs:
+ cert_names.append(cert['name'] + ".pem")
+ file = CsFile("%s/%s.pem" % (SSL_CERTS_DIR, cert['name']))
+ file.empty()
+ file.add("%s\n" % cert['cert'].replace("\r\n", "\n"))
+ if 'chain' in cert.keys():
+ file.add("%s\n" % cert['chain'].replace("\r\n", "\n"))
+ file.add("%s\n" % cert['key'].replace("\r\n", "\n"))
+ file.commit()
+ for f in listdir(SSL_CERTS_DIR):
+ if f not in cert_names:
+ CsHelper.execute("rm -rf %s/%s" % (SSL_CERTS_DIR, f))
+
+ def _verify_haproxy_config(self, config):
+ ret = CsHelper.execute2("haproxy -c -f %s" % config)
+ if ret.returncode:
+ stdout, stderr = ret.communicate()
+ logging.error("haproxy config is invalid with error: %s" % stderr)
+ return stderr
+ return ""
diff --git a/test/integration/smoke/test_ssl_offloading.py b/test/integration/smoke/test_ssl_offloading.py
new file mode 100644
index 0000000..5f0ea9a
--- /dev/null
+++ b/test/integration/smoke/test_ssl_offloading.py
@@ -0,0 +1,568 @@
+# 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.
+
+from marvin.codes import FAILED
+from marvin.cloudstackTestCase import cloudstackTestCase
+from marvin.lib.utils import wait_until
+from marvin.lib.base import (Account,
+ Project,
+ UserData,
+ SslCertificate,
+ Template,
+ NetworkOffering,
+ ServiceOffering,
+ VirtualMachine,
+ Network,
+ VPC,
+ VpcOffering,
+ PublicIPAddress,
+ LoadBalancerRule)
+from marvin.lib.common import (get_domain, get_zone, get_test_template)
+from nose.plugins.attrib import attr
+
+import os
+import subprocess
+import logging
+
+
+_multiprocess_shared_ = True
+
+DOMAIN = "test-ssl-offloading.cloudstack.org"
+CONTENT = "Test page"
+FULL_CHAIN = "/tmp/full_chain.crt"
+
+CERT = {
+ "privatekey": """-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCph7jsoMCQirRn
+3obuvgnnefTXRQYd9tF9k2aCVkTiiisvC39px7MGdgvDXADhD9fmR7oyXVQlfNu0
+rXjjgsVT3r4bv+DVi81YGXnuU7h10yCOZJt21i6QGHN1CS0/TAfg0UhlACCEYNRx
+kB0klwUcj/jk/AKil1DoUGpvAm2gZsek/njb76/AeIfxc+Es4ZOPCVqQOHp6gI0q
+t6KDMkUwv8fyzrpScygMUPVYrLmm6D0pn8yd3ihW07wGxMjND6UgOnao8t6H3LaM
+Pe7eqSFzxunF9NFFjnUrKcHZZSledDM/37Kbqb/8T5f+4SwjioS1OdPCh8ApdiXq
+HNUwYkALAgMBAAECggEAK5JiiQ7X7053B6s96uaVDRVfRGTNKa5iMXBNDHq3wbHZ
+X4IJAVr+PE7ivxdKco3r45fT11X9ZpUsssdTJsZZiTDak69BTiFcaaRCnmqOIlpd
+J7vb6TMrTIW8RvxQ0M/txm6DuNHLibqJX5a2pszZ13l5cwECfF9/v/XLJTTukCbu
+6D/f3fBVFl1tM8y9saOEYLkdb4dILWY61bVSDNswgprz2EV1SFnk5jxz2FuBrM/Q
++7hINvjDcaRvcm59hRb1rkljv7S10VoNw/CFkU451csJkUe4vWZwB8lZK/XxLQG0
+HEdS1zU1XY8H8Y1RCrxjGRyiiWsBtUThhWYlPrGCoQKBgQDkP09YAlKqXhT69Kx5
+keg2i1jV2hA73zWbWXt9xp5jG5r3pl3m170DvKL93YIDnHtpTC56mlzGrzS7DSTN
+p0buY9Qb3fkJxunCpPVFo0HMFkpeR77ax0v34NzSohlRLKFo5R2M1cmDfbVbnSSl
+MB57FfRRMxzjrk+dJvjOeJsxjwKBgQC+JLb4B8CZjpurXYg3ySiRqFsCqkqob+kf
+9dR+rWvcR6vMTEyha0hUlDvTikDepU2smYR4oPHfdcXF9lAJ7T02UmQDeizAqR68
+u9e+yS0q3tdRnPPZmXJfaDCXG1hKMqF4YA5Vs0XAjleF3zHB+vBLrnlPpShtd/Mu
+sWTpxICTxQKBgQDSr/n+pE5IQwYczOO0aFGwn5pF9L9NdPHXz5aleETV+TJn7WL6
+ZiRsoaDWs7SCvtxQS2kP9RM0t5/2FeDmEMXx4aZ2fsSWGM3IxVo+iL+Aswa81n8/
+Ff5y9lb/+29hNdBcsjk/ukwEG3Lf+UNNVAie15oppgPByzJkPwgmFsAy0wKBgHDX
+/TZp82WuerhSw/rHiSoYjhqg0bnw4Ju1Gy0q4q5SYqTWS0wpDT4U0wSSMjlwRQ6/
+9RxZ9/G0RXFc4tdhUkig0PY3VcPpGnLL0BhL8GBW69ZlnVpwdK4meV/UPKucLLPx
+3dACmszSLSMn+LG0qVNg8mHQFJQS8eGuKcOKePw5AoGACuxtefROKdKOALh4lTi2
+VOwPZ+1jxsm6lKNccIEvbUpe3UXPgNWpJiDX8mUcob4/NBLzmV3BUVKbG7Exbo5J
+LoMfp7OsztWUFwt7YAvRfS8fHdhkEsxEf3T72ADieH5ZAuXFF+K0H3r6HtWPD4ws
+mTJjGP4+Bl/dFakA5FJcjHg=
+-----END PRIVATE KEY-----""",
+ "certificate": """-----BEGIN CERTIFICATE-----
+MIIFKjCCAxKgAwIBAgIUJ7BtN56KI8OuzbbM8SdtCLCB2UgwDQYJKoZIhvcNAQEL
+BQAwXjELMAkGA1UEBhMCWFgxCzAJBgNVBAgMAlhYMQswCQYDVQQHDAJYWDETMBEG
+A1UECgwKQ2xvdWRTdGFjazEPMA0GA1UECwwGQXBhY2hlMQ8wDQYDVQQDDAZBcGFj
+aGUwHhcNMjUwNjIzMTMxMzA3WhcNMzUwNjIxMTMxMzA3WjBoMQswCQYDVQQGEwJY
+WDELMAkGA1UECAwCWFgxCzAJBgNVBAcMAlhYMQ8wDQYDVQQKDAZBcGFjaGUxEzAR
+BgNVBAsMCkNsb3VkU3RhY2sxGTAXBgNVBAMMECouY2xvdWRzdGFjay5vcmcwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCph7jsoMCQirRn3obuvgnnefTX
+RQYd9tF9k2aCVkTiiisvC39px7MGdgvDXADhD9fmR7oyXVQlfNu0rXjjgsVT3r4b
+v+DVi81YGXnuU7h10yCOZJt21i6QGHN1CS0/TAfg0UhlACCEYNRxkB0klwUcj/jk
+/AKil1DoUGpvAm2gZsek/njb76/AeIfxc+Es4ZOPCVqQOHp6gI0qt6KDMkUwv8fy
+zrpScygMUPVYrLmm6D0pn8yd3ihW07wGxMjND6UgOnao8t6H3LaMPe7eqSFzxunF
+9NFFjnUrKcHZZSledDM/37Kbqb/8T5f+4SwjioS1OdPCh8ApdiXqHNUwYkALAgMB
+AAGjgdUwgdIwKwYDVR0RBCQwIoIQKi5jbG91ZHN0YWNrLm9yZ4IOY2xvdWRzdGFj
+ay5vcmcwHQYDVR0OBBYEFCcq7jrdsqTD+Xi85DCqjYdL1gOqMIGDBgNVHSMEfDB6
+oWKkYDBeMQswCQYDVQQGEwJYWDELMAkGA1UECAwCWFgxCzAJBgNVBAcMAlhYMRMw
+EQYDVQQKDApDbG91ZFN0YWNrMQ8wDQYDVQQLDAZBcGFjaGUxDzANBgNVBAMMBkFw
+YWNoZYIURVB9+qvRJyOnJnqmYOw467vW3vQwDQYJKoZIhvcNAQELBQADggIBACld
+lEXgn/A4/kZQbLwwMxBvaoPDDaDaYVpPbOoPw7a8YkrL0rmPIc04PyX9GAqxdC+c
+qaEXvmp3I+BdT13XGcBosXO8uEQ3kses9F3MhOHORPS2mJag7t4eLnNX/0CgKTlR
+6yC2Gu7d3xPNJ+CKMxekdoF31StEFNAYI/La/q3D+IGsRCbrVu3xpPaw2XlXI7Ro
+RU7yebVmQPSNc75bm8Ydo1cdYtz9h8PVnc+6ThhSrdS3jYScj9DrX5ZJaKuZqSlu
+0ZqFXoBflme+cYB7nb9HqnIO67r9vzd2dTcErJVAk5jQqG5Y38d1tingDx1A5opU
+z4BkXEbHNV6VXYUQ5VE0dXO2sNvXVJrstwMPE8d3EvbX/1gWj8kuymbskrCjySE4
+4Yztkb0dsJkVU793lz3EV75DsXvj3gevK049nPv2Grt1+rTgFNa6NJnLvKIKk/mv
+fWjxbK2b/AAJ1ci6xtw/vKmIWoEu6uEMIJmhfBwuP+VnVJWJbmYXpNW/L5g21B76
+Fn8RuQa3mlm5lZrxEcJ/b6fF+2NPJwj7sh6l688VtNXoVSSyXUeV5HwqCv+YMjKn
+CtwpEN/eNHMbrkJvgYwSoOzqhV/wpmNi28S7MOm66JMECHOXOhk/eX2chIEjiVna
+MXhvr/Twfj2N4gNVtcgXkrk39HEYjk5+uF7SdNf4
+-----END CERTIFICATE-----""",
+ "certchain": """-----BEGIN CERTIFICATE-----
+MIIFQzCCAysCFEVQffqr0ScjpyZ6pmDsOOu71t70MA0GCSqGSIb3DQEBCwUAMF4x
+CzAJBgNVBAYTAlhYMQswCQYDVQQIDAJYWDELMAkGA1UEBwwCWFgxEzARBgNVBAoM
+CkNsb3VkU3RhY2sxDzANBgNVBAsMBkFwYWNoZTEPMA0GA1UEAwwGQXBhY2hlMB4X
+DTI1MDYxNjEwMjc1NloXDTMwMDYxNTEwMjc1NlowXjELMAkGA1UEBhMCWFgxCzAJ
+BgNVBAgMAlhYMQswCQYDVQQHDAJYWDETMBEGA1UECgwKQ2xvdWRTdGFjazEPMA0G
+A1UECwwGQXBhY2hlMQ8wDQYDVQQDDAZBcGFjaGUwggIiMA0GCSqGSIb3DQEBAQUA
+A4ICDwAwggIKAoICAQCLiQmSjrht15R1F+r79m/LZN5hsfQBGp+dy+yrtsWfOOur
+RdXAwgbLxxsyKMQKWCQxlRI7wdhqh0L0ZBrIr9MjltYqsqLAoLmgY4eG/f6G8YGr
+O/rxzfwTLbCeaIseF/OMA6Sz125HXYp1bltYK4LsuC7tihZXbeVa5pUGs3Jwgcfx
+LYm4eB42Hp7Eg05uL8LbwT/1AjcwoWkTewKAWXA83zgLRDFDbl1t0IPHI4cdVvia
+BNwNbG49ZCF6OgmokSarQSe4Vbems1u9T9pAySXAVjEYBqFjKWyswpdr782uNLmB
+lCGm0pDeJ9/WASxbTJr7k9H6ZpnaHr54DG6ZqennWMz8w6r2pf7bp/EGZ3mZQ4s3
+5ylSP4cQt8CSSI8k2CflPGUyytUAiWlDS3qSyIuAOPKXDg7wIpcbwcu4VMeKnH0Z
+x7Uu9j1UDZEZoSu6UI/VInTl47k1/ECD+AO9yBzZSv+pTQmO3/Im3CcxsTHmVd5s
+Tl0CJ/jWNpo9DAMtmGvt6CBWBXGRsO2XNk7djRcq2CubiCpvODg+7CcR6CiZK73L
+1aOisLiq3+ofiJSSXRRuKtJlkQ4eSPSbYWkNJcKmIhbCoYOdH/Pe3/+RHjvNc1kO
+OUb+icmfzcMVAs3C5jybpazsfjDNQZXWAFx4FLDcqOVbrCwom+tMukw+hzlZnwID
+AQABMA0GCSqGSIb3DQEBCwUAA4ICAQAdexoMwn+Ol1A3+NOHk9WJZX+t2Q8/9wWb
+K+jSVleSfXXWsB1mC3fABVJQdCxtXCH3rpt4w7FK6aUes9VjqAHap4tt9WBq0Wqq
+vvMURFHfllxEM31Z35kBOCSQY9xwpGV1rRh/zYs4h55YixomR3nXNZ9cI8xzlSCi
+sMG0mv0y+yxPohKrZj3AzLYz/M11SimSoyRPIANI+cUg1nJXyQoHzVHWEp1Nj0HB
+M/GW05cxsWea7f5YcAW1JQI3FOkpwb72fIZOtMDa4PO8IYWXJAeAc/chw745/MTi
+Rvl2NT4RZBAcrSNbhCOzRPG/ZiG+ArQuCluZ9HHAXRBMTtlLk5DO4+XxZlyGpjwf
+uKniK8dccy9uU0ho73p9SNDhXH0yb9Naj8vd9NWzCUYaaBXt/92cIyhaAHAVFxJu
+o6jr2FLbnhSGF9EO/tHvF7LxZv1dnbInvlWHwoFQjwmoeB+e17lHBdPMnWnPKBZe
+jA2VH/IzGCucWuWQhruummO5GT8Z6F4jBwvafBo+QARKPZgEBpx3LycXrpkYI3LT
+GGOpGCxFt5tVZOEsC/jQ5rIljNSeTzWmzfNRn/yRUW97uWsrzcQIBAUtu/pQnyFQ
+WCnC1ipCp1zhJsXAFUKuqEfLngXodOvC4tAOr76h11S57o5lN4506Poq2mWgAZe/
+JZr9MEn1+w==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFnzCCA4egAwIBAgIUcUNMqgWoDLsvMj0YmEudj60EG5swDQYJKoZIhvcNAQEL
+BQAwXjELMAkGA1UEBhMCWFgxCzAJBgNVBAgMAlhYMQswCQYDVQQHDAJYWDETMBEG
+A1UECgwKQ2xvdWRTdGFjazEPMA0GA1UECwwGQXBhY2hlMQ8wDQYDVQQDDAZBcGFj
+aGUwIBcNMjUwNjE2MTAyNzM2WhgPMjEyNTA1MjMxMDI3MzZaMF4xCzAJBgNVBAYT
+AlhYMQswCQYDVQQIDAJYWDELMAkGA1UEBwwCWFgxEzARBgNVBAoMCkNsb3VkU3Rh
+Y2sxDzANBgNVBAsMBkFwYWNoZTEPMA0GA1UEAwwGQXBhY2hlMIICIjANBgkqhkiG
+9w0BAQEFAAOCAg8AMIICCgKCAgEAwVQaePulUM523gKw168ToYp+gt05bXbu4Gg8
+uaRDKhnRAX1sEgYwkQ36Q+iTDEM9sKRma8lMNMIqkZMQdk6sIGX6BL+6wUOb7mL0
+5+I0yO9i8ooaGgNaeNvZftNIRlLsnPMGJaeom2/66XV4CsMqoZKaJ1H/I8N+bAeD
+GvrBx+B4l9D3G390nQvot9JUzrJgGuLl0KDHapvhlR39cCgEfIii02uX1iy0qXlV
+b+G1kLvpeC7T+lsJxondPJ69aO3lbDv/izyWw7qqBC57UhT/oKDxJmjQqklqzhgt
+nM/p3YE7M0nkRi3LnRmsZBz7o1DRf+M29zypKzXVk1aJflL46AtLMmpDIzVrEB2M
+q7o47rstXusYRYsBCqGTgdI1fV/CkDsZY5XkPZh2dsjZCHIS4P03OqFGsc6PQha2
++y2AhV1pvywkDl48kPKSukHfV1RtaPZUZtcQKztwHH+aFfo9mD8z0H2HcExdXKzd
+jhRhI9ZSwFj3HEN9f5P8fS3lf5+fV7EEbG4NisieBj/UivW6QiTHpLD7wRLIUt2g
+XgXNF0lfJzYHbIcxQ6kfC5McU2fu6mUC+p/pNN8G0POS3S2T55tEUqLL4N0SadQy
+N1TZlTd2xTn+Hb6WlG0f5m97xGcNlGHKBvntFrHvOIfkEQ9ne3MlOO1Gjlintowo
+fRGf15kCAwEAAaNTMFEwHQYDVR0OBBYEFM4WEQJpN9M07Q8CHq+5owG93Dj8MB8G
+A1UdIwQYMBaAFM4WEQJpN9M07Q8CHq+5owG93Dj8MA8GA1UdEwEB/wQFMAMBAf8w
+DQYJKoZIhvcNAQELBQADggIBABr5RKGc/rKx4fOgyXNJR4aCJCTtPZKs4AUCCBYz
+cWOjJYGNvThOPSVx51nAD8+YP2BwkOIPfhl2u/+vQSSbz4OlauXLki2DUN8E2OFe
+gJfxPDzWOfAo/QOJcyHwSlnIQjiZzG2lK3eyf5IFnfQILQzDvXaUYvMBkl2hb5Q7
+44H6tRw78uuf/KsT4rY0bBFMN5DayjiyvoIDUvzCRqcb2KOi9DnZ7pXjduL7tO0j
+PhlQ24B77LVUUAvydIGUzmbhGC2VvY1qE7uaYgYtgSUZ0zSjJrHjUjVLMzRouNP7
+jpbBQRAcP4FDcOFZBHogunA0hxQdm0d8u3LqDYPNS0rpfW0ddU/72nfBX4bnoDEN
++anw4wOgFuUcoEThALWZ9ESVKxXQ9Fpvd6FRW8fLLqhXAuli1BqP1c1WRxagldYe
+nPGm/FGZyJ2xOak9Uigi9NAQ/vX6CEfgcJgFZmCo8EKH0d4Ut72vGUcPqiUhT2EI
+AFAd6drSyoUdXXniSMWky9Vrt+qtLuAD1nhHTv8ZPdItXokoiD6ea/4xrbUZn0qY
+lLMDyfY76UVF0ruTR2Q6IdSq/zSggdwgkTooOW4XZcRf5l/ZnoeVQ1QH9C85SIKH
+IKZwPeGUm+EntmpuCBDmQSHLRCGEThd64iOAjqLR6arLj4TBJzBrZsGHFJbm0OcI
+dwa9
+-----END CERTIFICATE-----""",
+ "enabledrevocationcheck": False
+}
+
+# Install apache2 via userdata
+USER_DATA="""I2Nsb3VkLWNvbmZpZwpydW5jbWQ6CiAgLSBzdWRvIGFwdC1nZXQgdXBkYXRlCiAgLSBzdWRvIGFw
+dC1nZXQgaW5zdGFsbCAteSBhcGFjaGUyCiAgLSBzdWRvIHN5c3RlbWN0bCBlbmFibGUgYXBhY2hl
+MgogIC0gc3VkbyBzeXN0ZW1jdGwgc3RhcnQgYXBhY2hlMgogIC0gZWNobyAiVGVzdCBwYWdlIiB8
+c3VkbyB0ZWUgL3Zhci93d3cvaHRtbC90ZXN0Lmh0bWwK"""
+# #cloud-config
+# runcmd:
+# - sudo apt-get update
+# - sudo apt-get install -y apache2
+# - sudo systemctl enable apache2
+# - sudo systemctl start apache2
+# - echo "Test page" |sudo tee /var/www/html/test.html
+
+class TestSslOffloading(cloudstackTestCase):
+
+ @classmethod
+ def setUpClass(cls):
+
+ testClient = super(TestSslOffloading, cls).getClsTestClient()
+ cls.apiclient = testClient.getApiClient()
+ cls.services = testClient.getParsedTestDataConfig()
+ cls._cleanup = []
+
+ # Get Zone, Domain and templates
+ cls.domain = get_domain(cls.apiclient)
+ cls.zone = get_zone(cls.apiclient, testClient.getZoneForTests())
+ cls.hypervisor = testClient.getHypervisorInfo()
+
+ cls.services["virtual_machine"]["zoneid"] = cls.zone.id
+
+ # Save full chain as a file
+ with open(FULL_CHAIN, "w", encoding="utf-8") as f:
+ f.write(CERT["certchain"])
+
+ # Register template if needed
+ if cls.hypervisor.lower() == 'simulator':
+ cls.template = get_test_template(
+ cls.apiclient,
+ cls.zone.id,
+ cls.hypervisor)
+ else:
+ cls.template = Template.register(
+ cls.apiclient,
+ cls.services["test_templates_cloud_init"][cls.hypervisor.lower()],
+ zoneid=cls.zone.id,
+ hypervisor=cls.hypervisor,
+ )
+ cls.template.download(cls.apiclient)
+ cls._cleanup.append(cls.template)
+
+ if cls.template == FAILED:
+ assert False, "get_test_template() failed to return template"
+
+ # Create service offering
+ cls.service_offering = ServiceOffering.create(
+ cls.apiclient,
+ cls.services["service_offerings"]["big"] # 512MB memory
+ )
+ cls._cleanup.append(cls.service_offering)
+
+ # Create network offering
+ cls.services["isolated_network_offering"]["egress_policy"] = "true"
+ cls.network_offering = NetworkOffering.create(cls.apiclient,
+ cls.services["isolated_network_offering"],
+ conservemode=True)
+ cls.network_offering.update(cls.apiclient, state='Enabled')
+
+ cls._cleanup.append(cls.network_offering)
+
+ #Create an account, network, VM and IP addresses
+ cls.account = Account.create(
+ cls.apiclient,
+ cls.services["account"],
+ admin=True,
+ domainid=cls.domain.id
+ )
+ cls._cleanup.append(cls.account)
+ cls.user = cls.account.user[0]
+ cls.userapiclient = cls.testClient.getUserApiClient(cls.user.username, cls.domain.name)
+
+ cls.logger = logging.getLogger("TestSslOffloading")
+ cls.stream_handler = logging.StreamHandler()
+ cls.logger.setLevel(logging.DEBUG)
+ cls.logger.addHandler(cls.stream_handler)
+
+ def setUp(self):
+ self.apiclient = self.testClient.getApiClient()
+ self.cleanup = []
+
+ def tearDown(self):
+ super(TestSslOffloading, self).tearDown()
+
+ @classmethod
+ def tearDownClass(cls):
+ super(TestSslOffloading, cls).tearDownClass()
+ # Remove full chain file
+ if os.path.exists(FULL_CHAIN):
+ os.remove(FULL_CHAIN)
+
+ def wait_for_service_ready(self, command, expected, retries=60):
+ output = None
+ self.logger.debug("======================================")
+ self.logger.debug("Checking output of command '%s', expected result: '%s'" % (command, expected))
+ def check_output():
+ try:
+ output = subprocess.check_output(command + ' 2>&1', shell=True).strip().decode('utf-8')
+ except Exception as e:
+ self.logger.debug("Failed to get output of command '%s': '%s'" % (command, e))
+ if expected is None:
+ self.logger.debug("But it is expected")
+ return True, None
+ return False, None
+ self.logger.debug("Output of command '%s' is '%s'" % (command, output))
+ if expected is None:
+ self.logger.debug("But it is expected to be None")
+ return False, None
+ return (expected in output), None
+
+ res, _ = wait_until(10, retries, check_output)
+ if not res:
+ self.fail("Failed to wait for http server to show content '%s'. The output is '%s'" % (expected, output))
+
+ @attr(tags = ["advanced", "advancedns", "smoke"], required_hardware="true")
+ def test_01_ssl_offloading_isolated_network(self):
+ """Test to create Load balancing rule with SSL offloading"""
+
+ # Validate:
+ # 1. Create isolated network and vm instance
+ # 2. create LB with port 80 -> 80, verify the website (should get expected content)
+ # 3. create LB with port 443 -> 80, verify the website (should not work)
+ # 4. add cert to LB with port 443
+ # 5. verify the website (should get expected content)
+ # 6. remove cert from LB with port 443
+ # 7. delete SSL certificate
+
+ # Register Userdata
+ self.userdata = UserData.register(self.apiclient,
+ name="test-userdata",
+ userdata=USER_DATA,
+ account=self.account.name,
+ domainid=self.account.domainid
+ )
+
+ # Upload SSL Certificate
+ self.sslcert = SslCertificate.create(self.apiclient,
+ CERT,
+ name="test-ssl-certificate",
+ account=self.account.name,
+ domainid=self.account.domainid)
+
+ # 1. Create network
+ self.network = Network.create(self.apiclient,
+ zoneid=self.zone.id,
+ services=self.services["network"],
+ domainid=self.domain.id,
+ account=self.account.name,
+ networkofferingid=self.network_offering.id)
+ self.cleanup.append(self.network)
+
+ self.services["virtual_machine"]["networkids"] = [str(self.network.id)]
+
+ # Create vm instance
+ self.vm_1 = VirtualMachine.create(
+ self.apiclient,
+ self.services["virtual_machine"],
+ templateid=self.template.id,
+ accountid=self.account.name,
+ domainid=self.account.domainid,
+ userdataid=self.userdata.userdata.id,
+ serviceofferingid=self.service_offering.id
+ )
+ self.cleanup.append(self.vm_1)
+
+ self.public_ip = PublicIPAddress.create(
+ self.apiclient,
+ self.account.name,
+ self.zone.id,
+ self.account.domainid,
+ self.services["virtual_machine"],
+ self.network.id)
+
+ # 2. create LB with port 80 -> 80, verify the website (should get expected content).
+ # firewall is open by default
+ lb_http = {
+ "name": "http",
+ "alg": "roundrobin",
+ "privateport": 80,
+ "publicport": 80,
+ "protocol": "tcp"
+ }
+ lb_rule_http = LoadBalancerRule.create(
+ self.apiclient,
+ lb_http,
+ self.public_ip.ipaddress.id,
+ accountid=self.account.name,
+ domainid=self.domain.id,
+ networkid=self.network.id
+ )
+ lb_rule_http.assign(self.apiclient, [self.vm_1])
+ command = "curl -sL --connect-timeout 3 http://%s/test.html" % self.public_ip.ipaddress.ipaddress
+ # wait 10 minutes until the webpage is available. it returns "503 Service Unavailable" if not available
+ self.wait_for_service_ready(command, CONTENT, 60)
+
+ # 3. create LB with port 443 -> 80, verify the website (should not work)
+ # firewall is open by default
+ lb_https = {
+ "name": "https",
+ "alg": "roundrobin",
+ "privateport": 80,
+ "publicport": 443,
+ "protocol": "ssl"
+ }
+ lb_rule_https = LoadBalancerRule.create(
+ self.apiclient,
+ lb_https,
+ self.public_ip.ipaddress.id,
+ accountid=self.account.name,
+ domainid=self.domain.id,
+ networkid=self.network.id
+ )
+ lb_rule_https.assign(self.apiclient, [self.vm_1])
+
+ command = "curl -L --connect-timeout 3 -k --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+ self.wait_for_service_ready(command, None, 1)
+
+ command = "curl -L --connect-timeout 3 --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+ self.wait_for_service_ready(command, None, 1)
+
+ # 4. add cert to LB with port 443
+ lb_rule_https.assignCert(self.apiclient, self.sslcert.id)
+
+ # 5. verify the website (should get expected content)
+ command = "curl -L --connect-timeout 3 --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+ self.wait_for_service_ready(command, None, 1)
+
+ command = "curl -sL --connect-timeout 3 -k --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+ self.wait_for_service_ready(command, CONTENT, 1)
+
+ command = "curl -sL --connect-timeout 3 --cacert %s --resolve %s:443:%s https://%s/test.html" % (FULL_CHAIN, DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+ self.wait_for_service_ready(command, CONTENT, 1)
+
+ # 6. remove cert from LB with port 443
+ lb_rule_https.removeCert(self.apiclient)
+
+ # 7. delete SSL certificate
+ self.sslcert.delete(self.apiclient)
+
+ @attr(tags = ["advanced", "advancedns", "smoke"], required_hardware="true")
+ def test_02_ssl_offloading_project_vpc(self):
+ """Test to create Load balancing rule with SSL offloading in VPC in user project"""
+
+ # Validate:
+ # 1. Create VPC, VPC tier and vm instance
+ # 2. create LB with port 80 -> 80, verify the website (should get expected content)
+ # 3. create LB with port 443 -> 80, verify the website (should not work)
+ # 4. add cert to LB with port 443
+ # 5. verify the website (should get expected content)
+ # 6. remove cert from LB with port 443
+ # 7. delete SSL certificate
+
+ # Create project by user
+ self.project = Project.create(
+ self.userapiclient,
+ self.services["project"]
+ )
+ self.cleanup.append(self.project)
+
+ # Register Userdata by user
+ self.userdata = UserData.register(self.userapiclient,
+ name="test-user-userdata",
+ userdata=USER_DATA,
+ projectid=self.project.id
+ )
+
+ # Upload SSL Certificate by user
+ self.sslcert = SslCertificate.create(self.userapiclient,
+ CERT,
+ name="test-user-ssl-certificate",
+ projectid=self.project.id
+ )
+
+ # 1. Create VPC and VPC tier
+ vpcOffering = VpcOffering.list(self.userapiclient, name="Default VPC offering")
+ self.assertTrue(vpcOffering is not None and len(
+ vpcOffering) > 0, "No VPC offerings found")
+
+ self.vpc = VPC.create(
+ apiclient=self.userapiclient,
+ services=self.services["vpc_vpn"]["vpc"],
+ vpcofferingid=vpcOffering[0].id,
+ zoneid=self.zone.id,
+ projectid=self.project.id
+ )
+ self.cleanup.append(self.vpc)
+
+ networkOffering = NetworkOffering.list(
+ self.userapiclient, name="DefaultIsolatedNetworkOfferingForVpcNetworks")
+ self.assertTrue(networkOffering is not None and len(
+ networkOffering) > 0, "No VPC based network offering")
+
+ self.network = Network.create(
+ apiclient=self.userapiclient,
+ services=self.services["vpc_vpn"]["network_1"],
+ networkofferingid=networkOffering[0].id,
+ zoneid=self.zone.id,
+ vpcid=self.vpc.id,
+ projectid=self.project.id
+ )
+ self.cleanup.append(self.network)
+
+ self.services["virtual_machine"]["networkids"] = [str(self.network.id)]
+
+ # Create vm instance
+ self.vm_2 = VirtualMachine.create(
+ self.userapiclient,
+ self.services["virtual_machine"],
+ templateid=self.template.id,
+ userdataid=self.userdata.userdata.id,
+ serviceofferingid=self.service_offering.id,
+ projectid=self.project.id
+ )
+ self.cleanup.append(self.vm_2)
+
+ self.public_ip = PublicIPAddress.create(
+ self.userapiclient,
+ zoneid=self.zone.id,
+ services=self.services["virtual_machine"],
+ networkid=self.network.id,
+ vpcid=self.vpc.id,
+ projectid=self.project.id
+ )
+
+ # 2. create LB with port 80 -> 80, verify the website (should get expected content).
+ # firewall is open by default
+ lb_http = {
+ "name": "http",
+ "alg": "roundrobin",
+ "privateport": 80,
+ "publicport": 80,
+ "protocol": "tcp"
+ }
+ lb_rule_http = LoadBalancerRule.create(
+ self.userapiclient,
+ lb_http,
+ self.public_ip.ipaddress.id,
+ networkid=self.network.id,
+ projectid=self.project.id
+ )
+ lb_rule_http.assign(self.userapiclient, [self.vm_2])
+ command = "curl -sL --connect-timeout 3 http://%s/test.html" % self.public_ip.ipaddress.ipaddress
+ # wait 10 minutes until the webpage is available. it returns "503 Service Unavailable" if not available
+ self.wait_for_service_ready(command, CONTENT, 60)
+
+ # 3. create LB with port 443 -> 80, verify the website (should not work)
+ # firewall is open by default
+ lb_https = {
+ "name": "https",
+ "alg": "roundrobin",
+ "privateport": 80,
+ "publicport": 443,
+ "protocol": "ssl"
+ }
+ lb_rule_https = LoadBalancerRule.create(
+ self.userapiclient,
+ lb_https,
+ self.public_ip.ipaddress.id,
+ networkid=self.network.id,
+ projectid=self.project.id
+ )
+ lb_rule_https.assign(self.userapiclient, [self.vm_2])
+
+ command = "curl -L --connect-timeout 3 -k --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+ self.wait_for_service_ready(command, None, 1)
+
+ command = "curl -L --connect-timeout 3 --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+ self.wait_for_service_ready(command, None, 1)
+
+ # 4. add cert to LB with port 443
+ lb_rule_https.assignCert(self.userapiclient, self.sslcert.id)
+
+ # 5. verify the website (should get expected content)
+ command = "curl -L --connect-timeout 3 --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+ self.wait_for_service_ready(command, None, 1)
+
+ command = "curl -sL --connect-timeout 3 -k --resolve %s:443:%s https://%s/test.html" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+ self.wait_for_service_ready(command, CONTENT, 1)
+
+ command = "curl -sL --connect-timeout 3 --cacert %s --resolve %s:443:%s https://%s/test.html" % (FULL_CHAIN, DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN)
+ self.wait_for_service_ready(command, CONTENT, 1)
+
+ # 6. remove cert from LB with port 443
+ lb_rule_https.removeCert(self.userapiclient)
+
+ # 7. delete SSL certificate
+ self.sslcert.delete(self.userapiclient)
diff --git a/tools/marvin/marvin/cloudstackConnection.py b/tools/marvin/marvin/cloudstackConnection.py
index 5b438da..d64046b 100644
--- a/tools/marvin/marvin/cloudstackConnection.py
+++ b/tools/marvin/marvin/cloudstackConnection.py
@@ -164,9 +164,10 @@
'''
try:
response = requests.post(url,
- params=payload,
+ data=payload,
cert=self.certPath,
verify=self.httpsFlag)
+ self.logger.debug("=======Got POST response : %s=======" % response)
return response
except Exception as e:
self.__lastError = e
diff --git a/tools/marvin/marvin/config/test_data.py b/tools/marvin/marvin/config/test_data.py
index edacf16..e3d4022 100644
--- a/tools/marvin/marvin/config/test_data.py
+++ b/tools/marvin/marvin/config/test_data.py
@@ -1068,7 +1068,7 @@
"displaytext": "ubuntu 22.04 kvm",
"format": "raw",
"hypervisor": "kvm",
- "ostype": "Other Linux (64-bit)",
+ "ostype": "Ubuntu 22.04 LTS",
"url": "https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-amd64.img",
"requireshvm": "True",
"ispublic": "True",
diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py
index 16b2467..ac6e97a 100755
--- a/tools/marvin/marvin/lib/base.py
+++ b/tools/marvin/marvin/lib/base.py
@@ -3080,6 +3080,9 @@
if "openfirewall" in services:
cmd.openfirewall = services["openfirewall"]
+ if "protocol" in services:
+ cmd.protocol = services["protocol"]
+
if projectid:
cmd.projectid = projectid
@@ -3188,6 +3191,22 @@
[setattr(cmd, k, v) for k, v in list(kwargs.items())]
return apiclient.listLoadBalancerRuleInstances(cmd)
+ def assignCert(self, apiclient, certId, forced=None):
+ """"""
+ cmd = assignCertToLoadBalancer.assignCertToLoadBalancerCmd()
+ cmd.lbruleid = self.id
+ cmd.certid = certId
+ if forced is not None:
+ cmd.forced = forced
+ return apiclient.assignCertToLoadBalancer(cmd)
+
+ def removeCert(self, apiclient):
+ """Removes a certificate from a load balancer rule"""
+
+ cmd = removeCertFromLoadBalancer.removeCertFromLoadBalancerCmd()
+ cmd.lbruleid = self.id
+ return apiclient.removeCertFromLoadBalancer(cmd)
+
class Cluster:
"""Manage Cluster life cycle"""
@@ -8016,3 +8035,60 @@
cmd.id = self.id
[setattr(cmd, k, v) for k, v in list(kwargs.items())]
return (apiclient.updateGpuDevice(cmd))
+
+
+class SslCertificate:
+
+ def __init__(self, items):
+ self.__dict__.update(items)
+
+ @classmethod
+ def create(cls, apiclient, services, name, certificate=None, privatekey=None,
+ certchain=None, password=None, enabledrevocationcheck=None,
+ account=None, domainid=None, projectid=None):
+ """Upload SSL certificate"""
+ cmd = uploadSslCert.uploadSslCertCmd()
+ cmd.name = name
+
+ if certificate:
+ cmd.certificate = certificate
+ elif "certificate" in services:
+ cmd.certificate = services["certificate"]
+
+ if privatekey:
+ cmd.privatekey = privatekey
+ elif "privatekey" in services:
+ cmd.privatekey = services["privatekey"]
+
+ if certchain:
+ cmd.certchain = certchain
+ elif "certchain" in services:
+ cmd.certchain = services["certchain"]
+
+ if password:
+ cmd.password = password
+ elif "password" in services:
+ cmd.password = services["password"]
+
+ if enabledrevocationcheck is not None:
+ cmd.enabledrevocationcheck = enabledrevocationcheck
+ elif "enabledrevocationcheck" in services:
+ cmd.enabledrevocationcheck = services["enabledrevocationcheck"]
+
+ if account:
+ cmd.account = account
+
+ if projectid:
+ cmd.projectid = projectid
+
+ if domainid:
+ cmd.domainid = domainid
+
+ return SslCertificate(apiclient.uploadSslCert(cmd, method='POST').__dict__)
+
+ def delete(self, apiclient):
+ """Delete SSL Certificate"""
+
+ cmd = deleteSslCert.deleteSslCertCmd()
+ cmd.id = self.id
+ apiclient.deleteSslCert(cmd)
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 394de6c..5136f24 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -505,10 +505,12 @@
"label.category": "Category",
"label.certchain": "Chain",
"label.certificate": "Certificate",
+"label.certificate.chain": "Certificate chain",
"label.certificate.upload": "Certificate uploaded.",
"label.certificate.upload.failed": "Certificate upload failed",
"label.certificate.upload.failed.description": "Failed to update SSL Certificate. Failed to pass certificate validation check.",
"label.certificateid": "Certificate ID",
+"label.certificates": "Certificates",
"label.chainsize": "Chain size",
"label.change": "Change",
"label.change.affinity": "Change affinity",
@@ -972,6 +974,7 @@
"label.enable.vpn": "Enable remote access VPN",
"label.enable.webhook": "Enable Webhook",
"label.enabled": "Enabled",
+"label.enabled.revocation.check": "Enables revocation checking for certificates",
"label.encrypt": "Encrypt",
"label.encryptroot": "Encrypt Root Disk",
"label.end": "End",
@@ -1480,6 +1483,7 @@
"label.make.user.project.owner": "Make User project owner",
"label.makeredundant": "Make redundant",
"label.manage": "Manage",
+"label.manage.ssl.cert": "Manage SSL certificate",
"label.manage.vpn.user": "Manage VPN Users",
"label.managed.instances": "Managed Instances",
"label.managed.volumes": "Managed Volumes",
@@ -2053,6 +2057,7 @@
"label.remove.vpc.offering": "Remove VPC Offering",
"label.removed": "Removed",
"label.removing": "Removing",
+"label.replace": "Replace",
"label.replace.acl": "Replace ACL",
"label.report.bug": "Ask a question or Report an issue",
"label.request": "Request",
@@ -2312,6 +2317,7 @@
"label.uefi.supported": "UEFI supported",
"label.unregister.extension": "Unregister Extension",
"label.usediops": "IOPS used",
+"label.userdata": "User Data",
"label.user.data.id": "User Data ID",
"label.user.data.name": "User Data name",
"label.user.data.details": "User Data details",
@@ -2327,6 +2333,8 @@
"label.ssh.port": "SSH port",
"label.sshkeypair": "New SSH key pair",
"label.sshkeypairs": "SSH key pairs",
+"label.ssl": "SSL",
+"label.sslcertificate": "SSL certificate",
"label.sslcertificates": "SSL certificates",
"label.sslverification": "SSL verification",
"label.standard.us.keyboard": "Standard (US) keyboard",
@@ -2587,6 +2595,7 @@
"label.upload.icon": "Upload icon",
"label.upload.iso.from.local": "Upload ISO from local",
"label.upload.resource.icon": "Upload icon",
+"label.upload.ssl.certificate": "Upload SSL cerficicate",
"label.upload.template.from.local": "Upload Template from local",
"label.upload.volume": "Upload volume",
"label.upload.volume.from.local": "Upload Volume from local",
@@ -2996,6 +3005,8 @@
"message.remove.ip.v6.firewall.rule.failed": "Failed to remove IPv6 firewall rule",
"message.remove.ip.v6.firewall.rule.processing": "Removing IPv6 firewall rule...",
"message.remove.ip.v6.firewall.rule.success": "Removed IPv6 firewall rule",
+"message.remove.sslcert.failed": "Failed to remove SSL certificate from load balancer",
+"message.remove.sslcert.processing": "Removing SSL certificate from load balancer...",
"message.add.netris.controller": "Add Netris Provider",
"message.add.nsx.controller": "Add NSX Provider",
"message.add.network": "Add a new network for Zone: <b><span id=\"zone_name\"></span></b>",
@@ -3047,6 +3058,8 @@
"message.allowed": "Allowed",
"message.alert.show.all.stats.data": "This may return a lot of data depending on VM statistics and retention settings",
"message.apply.success": "Apply Successfully",
+"message.assign.sslcert.failed": "Failed to assign SSL certificate",
+"message.assign.sslcert.processing": "Assigning SSL certificate...",
"message.assign.instance.another": "Please specify the Account type, domain, Account name and Network (optional) of the new Account. <br> If the default NIC of the Instance is on a shared Network, CloudStack will check if the Network can be used by the new Account if you do not specify one Network. <br> If the default NIC of the Instance is on a isolated Network, and the new Account has more one isolated Networks, you should specify one.",
"message.assign.vm.failed": "Failed to assign Instance",
"message.assign.vm.processing": "Assigning Instance...",
@@ -3762,6 +3775,7 @@
"message.success.add.vpc.network": "Successfully added a VPC network",
"message.success.add.vpn.customer.gateway": "Successfully added VPN customer gateway",
"message.success.add.vpn.gateway": "Successfully added VPN gateway",
+"message.success.assign.sslcert": "Successfully assigned SSL certificate",
"message.success.assign.vm": "Successfully assigned Instance",
"message.success.apply.network.policy": "Successfully applied Network Policy",
"message.success.apply.tungsten.tag": "Successfully applied Tag",
@@ -3840,6 +3854,7 @@
"message.success.release.ip": "Successfully released IP",
"message.success.release.dedicated.bgp.peer": "Successfully released dedicated BGP peer",
"message.success.release.dedicated.ipv4.subnet": "Successfully released dedicated IPv4 subnet",
+"message.success.remove.sslcert": "Successfully removed SSL certificate from load balancer",
"message.success.remove.egress.rule": "Successfully removed egress rule",
"message.success.remove.objectstore.objects": "Successfully removed selected object(s)",
"message.success.remove.objectstore.directory": "Successfully removed selected directory",
@@ -3888,6 +3903,7 @@
"message.success.upload.description": "This ISO file has been uploaded. Please check its status in the Templates menu.",
"message.success.upload.icon": "Successfully uploaded icon for ",
"message.success.upload.iso.description": "This ISO file has been uploaded. Please check its status in the images > ISOs menu.",
+"message.success.upload.ssl.cert": "Successfully uploaded SSL certificate",
"message.success.upload.template.description": "This Template file has been uploaded. Please check its status in the Templates menu.",
"message.success.upload.volume.description": "This volume has been uploaded. Please check its status in the volumes menu.",
"message.suspend.project": "Are you sure you want to suspend this project?",
diff --git a/ui/src/config/section/account.js b/ui/src/config/section/account.js
index 55b950d..5766fd8 100644
--- a/ui/src/config/section/account.js
+++ b/ui/src/config/section/account.js
@@ -85,7 +85,7 @@
component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceLimitTab.vue')))
},
{
- name: 'certificate',
+ name: 'certificates',
component: shallowRef(defineAsyncComponent(() => import('@/views/iam/SSLCertificateTab.vue')))
},
{
diff --git a/ui/src/config/section/project.js b/ui/src/config/section/project.js
index 18354c3..5a1f5f7 100644
--- a/ui/src/config/section/project.js
+++ b/ui/src/config/section/project.js
@@ -47,6 +47,10 @@
}
},
{
+ name: 'certificates',
+ component: shallowRef(defineAsyncComponent(() => import('@/views/iam/SSLCertificateTab.vue')))
+ },
+ {
name: 'limits',
component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceCountUsage.vue')))
},
diff --git a/ui/src/views/compute/AutoScaleLoadBalancing.vue b/ui/src/views/compute/AutoScaleLoadBalancing.vue
index a24e928..6c04ce1 100644
--- a/ui/src/views/compute/AutoScaleLoadBalancing.vue
+++ b/ui/src/views/compute/AutoScaleLoadBalancing.vue
@@ -284,6 +284,7 @@
<a-select-option value="tcp-proxy">{{ $t('label.tcp.proxy') }}</a-select-option>
<a-select-option value="tcp">{{ $t('label.tcp') }}</a-select-option>
<a-select-option value="udp">{{ $t('label.udp') }}</a-select-option>
+ <a-select-option value="ssl">{{ $t('label.ssl') }}</a-select-option>
</a-select>
</div>
<div :span="24" class="action-button">
diff --git a/ui/src/views/iam/SSLCertificateTab.vue b/ui/src/views/iam/SSLCertificateTab.vue
index e5890ac..08f1ee4 100644
--- a/ui/src/views/iam/SSLCertificateTab.vue
+++ b/ui/src/views/iam/SSLCertificateTab.vue
@@ -18,6 +18,17 @@
<template>
<div>
<a-row :gutter="12">
+ <a-spin :spinning="loading">
+ <a-button
+ shape="round"
+ style="left: 10px; float: right;margin-bottom: 10px; z-index: 8"
+ @click="() => { showUploadForm = true }">
+ <template #icon><plus-outlined /></template>
+ {{ $t('label.upload.ssl.certificate') }}
+ </a-button>
+ </a-spin>
+ </a-row>
+ <a-row :gutter="12">
<a-col :md="24" :lg="24">
<a-table
size="small"
@@ -68,16 +79,120 @@
</a-list>
</a-col>
</a-row>
+
+ <a-modal
+ v-if="showUploadForm"
+ :visible="showUploadForm"
+ :title="$t('label.upload.ssl.certificate')"
+ :maskClosable="false"
+ :closable="true"
+ :footer="null"
+ @cancel="() => { showUploadForm = false }"
+ centered
+ width="30vw">
+
+ <a-form
+ layout="vertical"
+ :ref="formRef"
+ :model="form"
+ :rules="rules"
+ @finish="uploadSslCert"
+ v-ctrl-enter="uploadSslCert"
+ >
+ <a-form-item name="name" ref="name">
+ <template #label>
+ <tooltip-label :title="$t('label.name')" :tooltip="apiParams.name.description" tooltipPlacement="bottom"/>
+ </template>
+ <a-input
+ id="name"
+ :placeholder="apiParams.name.description"
+ name="name"
+ v-model:value="form.name"
+ ></a-input>
+ </a-form-item>
+
+ <a-form-item name="certificate" ref="certificate" :required="true">
+ <template #label>
+ <tooltip-label :title="$t('label.certificate')" :tooltip="apiParams.certificate.description" tooltipPlacement="bottom"/>
+ </template>
+ <a-textarea
+ id="certificate"
+ rows="2"
+ :placeholder="apiParams.certificate.description"
+ v-focus="true"
+ name="certificate"
+ v-model:value="form.certificate"
+ ></a-textarea>
+ </a-form-item>
+
+ <a-form-item name="privatekey" ref="privatekey" :required="true">
+ <template #label>
+ <tooltip-label :title="$t('label.privatekey')" :tooltip="apiParams.privatekey.description" tooltipPlacement="bottom"/>
+ </template>
+ <a-textarea
+ id="privatekey"
+ rows="2"
+ :placeholder="apiParams.privatekey.description"
+ name="privatekey"
+ v-model:value="form.privatekey"
+ ></a-textarea>
+ </a-form-item>
+
+ <a-form-item name="certchain" ref="certchain">
+ <template #label>
+ <tooltip-label :title="$t('label.certificate.chain')" :tooltip="apiParams.certchain.description" tooltipPlacement="bottom"/>
+ </template>
+ <a-textarea
+ id="certchain"
+ rows="2"
+ :placeholder="apiParams.certchain.description"
+ name="certchain"
+ v-model:value="form.certchain"
+ ></a-textarea>
+ </a-form-item>
+
+ <a-form-item name="password" ref="password">
+ <template #label>
+ <tooltip-label :title="$t('label.password')" :tooltip="apiParams.password.description" tooltipPlacement="bottom"/>
+ </template>
+ <a-input
+ type="password"
+ id="password"
+ name="password"
+ v-model:value="form.password"
+ ></a-input>
+ </a-form-item>
+
+ <a-form-item name="enabledrevocationcheck" ref="enabledrevocationcheck">
+ <template #label>
+ <tooltip-label :title="$t('label.enabled.revocation.check')" :tooltip="apiParams.enabledrevocationcheck.description" tooltipPlacement="bottom"/>
+ </template>
+ <a-checkbox v-model:checked="form.enabledrevocationcheck"></a-checkbox>
+ </a-form-item>
+
+ <div :span="24" class="action-button">
+ <a-button @click="showUploadForm = false" class="close-button">
+ {{ $t('label.cancel' ) }}
+ </a-button>
+ <a-button type="primary" ref="submit" :loading="uploading" @click="uploadSslCert">
+ {{ $t('label.submit' ) }}
+ </a-button>
+ </div>
+ </a-form>
+ </a-modal>
</div>
</template>
<script>
import { getAPI, postAPI } from '@/api'
import TooltipButton from '@/components/widgets/TooltipButton'
+import TooltipLabel from '@/components/widgets/TooltipLabel.vue'
+import { ref, reactive, toRaw } from 'vue'
export default {
name: 'SSLCertificate',
components: {
+ TooltipLabel,
TooltipButton
},
data () {
@@ -90,7 +205,9 @@
page: 1,
pageSize: 10,
quickview: false,
- loading: false
+ loading: false,
+ uploading: false,
+ showUploadForm: false
}
},
props: {
@@ -127,6 +244,9 @@
}
}
},
+ beforeCreate () {
+ this.apiParams = this.$getApiParams('uploadSslCert')
+ },
created () {
this.columns = [
{
@@ -149,14 +269,28 @@
}
]
this.detailColumn = ['name', 'certificate', 'certchain']
+ this.initForm()
this.fetchData()
},
methods: {
+ initForm () {
+ this.formRef = ref()
+ this.form = reactive({})
+ this.rules = reactive({
+ certificate: [{ required: true, message: this.$t('label.required') }],
+ privatekey: [{ required: true, message: this.$t('label.required') }]
+ })
+ },
fetchData () {
const params = {}
params.page = this.page
params.pageSize = this.pageSize
- params.accountid = this.resource.id
+ if (this.$route.meta.name === 'account') {
+ params.accountid = this.resource.id
+ delete params.projectid
+ } else { // project
+ params.projectid = this.resource.id
+ }
this.loading = true
@@ -224,6 +358,46 @@
self.onDelete(row)
}
})
+ },
+ uploadSslCert () {
+ if (this.uploading) return
+ this.formRef.value.validate().then(() => {
+ const formValues = toRaw(this.form)
+ this.uploading = true
+ const params = {
+ name: formValues.name,
+ certificate: formValues.certificate,
+ privatekey: formValues.privatekey
+ }
+ if (formValues.enabledrevocationcheck != null && formValues.enabledrevocationcheck) {
+ params.enabledrevocationcheck = 'true'
+ } else {
+ params.enabledrevocationcheck = 'false'
+ }
+ if (this.$route.meta.name === 'account') {
+ params.account = this.resource.name
+ params.domainid = this.resource.domainid
+ } else { // project
+ params.projectid = this.resource.id
+ }
+ if (formValues.password) {
+ params.password = formValues.password
+ }
+ if (formValues.certchain) {
+ params.certchain = formValues.certchain
+ }
+ postAPI('uploadSslCert', params).then(json => {
+ this.$notification.success({
+ message: this.$t('message.success.upload.ssl.cert')
+ })
+ }).catch(error => {
+ this.$notifyError(error)
+ }).finally(() => {
+ this.fetchData()
+ this.uploading = false
+ this.showUploadForm = false
+ })
+ })
}
}
}
diff --git a/ui/src/views/network/LoadBalancing.vue b/ui/src/views/network/LoadBalancing.vue
index 803371f..6d99522 100644
--- a/ui/src/views/network/LoadBalancing.vue
+++ b/ui/src/views/network/LoadBalancing.vue
@@ -67,6 +67,7 @@
<a-select-option v-if="lbProvider !== 'Netris'" value="tcp-proxy" :label="$t('label.tcp.proxy')">{{ $t('label.tcp.proxy') }}</a-select-option>
<a-select-option value="tcp" :label="$t('label.tcp')">{{ $t('label.tcp') }}</a-select-option>
<a-select-option value="udp" :label="$t('label.udp')">{{ $t('label.udp') }}</a-select-option>
+ <a-select-option value="ssl" :label="$t('label.ssl')">{{ $t('label.ssl') }}</a-select-option>
</a-select>
</div>
<div class="form__item">
@@ -84,6 +85,12 @@
<a-select-option value="no">{{ $t('label.no') }}</a-select-option>
</a-select>
</div>
+ <div class="form__item" v-if="newRule.protocol === 'ssl'" >
+ <div class="form__label">{{ $t('label.sslcertificate') }}</div>
+ <a-button :disabled="!('createLoadBalancerRule' in $store.getters.apis)" type="primary" @click="handleOpenAddSslCertModal(null)">
+ {{ this.selectedSsl.id != null ? this.selectedSsl.name : $t('label.add') }}
+ </a-button>
+ </div>
<div class="form__item" v-if="!newRule.autoscale || newRule.autoscale === 'no'">
<div class="form__label" style="white-space: nowrap;">{{ $t('label.add.vms') }}</div>
<a-button :disabled="!('createLoadBalancerRule' in $store.getters.apis)" type="primary" @click="handleOpenAddVMModal">
@@ -139,6 +146,12 @@
{{ returnStickinessLabel(record.id) }}
</a-button>
</template>
+ <template v-if="column.key === 'sslcert'">
+ <a-button :disabled="record.protocol !== 'ssl'" @click="() => { selectedRule = record; handleOpenAddSslCertModal(record) }">
+ <template #icon><plus-outlined /></template>
+ {{ $t('label.manage') }}
+ </a-button>
+ </template>
<template v-if="column.key === 'autoscale'">
<div>
<router-link :to="{ path: '/autoscalevmgroup/' + record.autoscalevmgroup.id }" v-if='record.autoscalevmgroup'>
@@ -435,6 +448,7 @@
<a-select-option value="tcp-proxy" :label="$t('label.tcp.proxy')">{{ $t('label.tcp.proxy') }}</a-select-option>
<a-select-option value="tcp" :label="$t('label.tcp')">{{ $t('label.tcp') }}</a-select-option>
<a-select-option value="udp" :label="$t('label.udp')">{{ $t('label.udp') }}</a-select-option>
+ <a-select-option value="ssl" :label="$t('label.ssl')">{{ $t('label.ssl') }}</a-select-option>
</a-select>
</div>
<div :span="24" class="action-button">
@@ -553,6 +567,60 @@
</a-modal>
<a-modal
+ :title="$t('label.manage.ssl.cert')"
+ :maskClosable="false"
+ :closable="true"
+ v-if="addSslCertModalVisible"
+ :visible="addSslCertModalVisible"
+ width="30vw"
+ @cancel="addSslCertModalVisible = false"
+ @ok="addSslCertModalVisible = false"
+ :cancelButtonProps="{ style: { display: 'none' } }"
+ >
+ <a-row v-show="showAssignedSsl && assignedSslCert !== 'None'">
+ <a-col :span="8">
+ <div class="form__label">{{ $t("label.current") + ' ' + $t('label.sslcertificate') }}</div>
+ </a-col>
+ <a-col :span="10">
+ <div>{{ assignedSslCert }}</div>
+ </a-col>
+ <a-col :span="6">
+ <a-button :disabled="!deleteSslButtonVisible" type="danger" @click="removeSslFromLbRule()">
+ <template #icon><delete-outlined /></template>
+ {{ $t('label.remove') }}
+ </a-button>
+ </a-col>
+ </a-row>
+ <a-row style="margin-top: 16px">
+ <a-col :span="8">
+ <div class="form__label">{{ $t("label.new") + ' ' + $t('label.sslcertificate') }}</div>
+ </a-col>
+ <a-col :span="10">
+ <div class="form__item">
+ <a-select v-model:value="selectedSsl.name" style="width: 80%;" @change="selectssl">
+ <a-select-option
+ v-for="sslcert in sslcerts.data"
+ :key="sslcert.id">{{ sslcert.name }}
+ </a-select-option>
+ </a-select>
+ </div>
+ </a-col>
+ <a-col :span="6">
+ <div>
+ <a-button v-show="addSslButtonVisible && assignedSslCert !== 'None'" type="primary" @click="addSslTolbRule()">
+ <template #icon><swap-outlined /></template>
+ {{ $t('label.replace') }}
+ </a-button>
+ <a-button v-show="addSslButtonVisible && assignedSslCert === 'None'" type="primary" @click="addSslTolbRule()">
+ <template #icon><plus-outlined /></template>
+ {{ $t('label.assign') }}
+ </a-button>
+ </div>
+ </a-col>
+ </a-row>
+ </a-modal>
+
+ <a-modal
:title="$t('label.select.tier')"
:maskClosable="false"
:closable="true"
@@ -791,6 +859,20 @@
totalCount: 0,
page: 1,
pageSize: 10,
+ sslcerts: {
+ loading: false,
+ data: []
+ },
+ selectedSsl: {
+ name: '',
+ id: null
+ },
+ addSslCertModalVisible: false,
+ showAssignedSsl: false,
+ currentAccountId: null,
+ assignedSslCert: 'None',
+ deleteSslButtonVisible: true,
+ addSslButtonVisible: true,
columns: [
{
title: this.$t('label.name'),
@@ -825,6 +907,10 @@
title: this.$t('label.add.vms')
},
{
+ key: 'sslcert',
+ title: this.$t('label.sslcertificate')
+ },
+ {
key: 'autoscale',
title: this.$t('label.autoscale')
},
@@ -1092,6 +1178,147 @@
}
})
},
+ fetchSslCerts () {
+ this.sslcerts.loading = true
+ this.sslcerts.data = []
+ // First get the account id
+ getAPI('listAccounts', {
+ name: this.resource.account,
+ domainid: this.resource.domainid
+ }).then(json => {
+ const accounts = json.listaccountsresponse.account || []
+ if (accounts.length > 0) {
+ // Now fetch all the ssl certs for this account
+ this.currentAccountId = accounts[0].id
+ getAPI('listSslCerts', {
+ accountid: this.currentAccountId
+ }).then(json => {
+ json.listsslcertsresponse.sslcert.forEach(entry => this.sslcerts.data.push(entry))
+ if (json.listsslcertsresponse.sslcert && json.listsslcertsresponse.sslcert.length > 0 && this.selectedSsl.id == null) {
+ this.selectedSsl.name = json.listsslcertsresponse.sslcert[0].name
+ this.selectedSsl.id = json.listsslcertsresponse.sslcert[0].id
+ }
+ }).catch(error => {
+ this.$notifyError(error)
+ })
+ }
+ }).catch(error => {
+ this.$notifyError(error)
+ }).finally(() => {
+ this.sslcerts.loading = false
+ })
+ if (this.selectedRule !== null) {
+ this.getCurrentAssignedSslCert()
+ }
+ },
+ getCurrentAssignedSslCert () {
+ getAPI('listSslCerts', {
+ accountid: this.currentAccountId,
+ lbruleid: this.selectedRule.id
+ }).then(json => {
+ if (json.listsslcertsresponse.sslcert && json.listsslcertsresponse.sslcert.length > 0) {
+ this.assignedSslCert = json.listsslcertsresponse.sslcert[0].name
+ this.deleteSslButtonVisible = true
+ } else {
+ this.assignedSslCert = 'None'
+ this.deleteSslButtonVisible = false
+ }
+ }).catch(error => {
+ this.$notifyError(error)
+ })
+ },
+ selectssl (e) {
+ this.selectedSsl.id = e
+ const sslcert = this.sslcerts.data.find(entry => entry.id === this.selectedSsl.id)
+ if (sslcert) {
+ this.selectedSsl.name = sslcert.name
+ }
+ },
+ handleAddSslCert (data) {
+ this.addSslCert(data, this.selectedSsl.id)
+ },
+ addSslTolbRule () {
+ this.visible = false
+ this.addSslCert(this.selectedRule.id, this.selectedSsl.id)
+ },
+ addSslCert (lbRuleId, certId) {
+ this.disableSslAddDeleteButtons()
+ getAPI('assignCertToLoadBalancer', {
+ lbruleid: lbRuleId,
+ certid: certId,
+ forced: true
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.assigncerttoloadbalancerresponse.jobid,
+ successMessage: this.$t('message.success.assign.sslcert'),
+ successMethod: () => {
+ if (this.selectedRule !== null) {
+ this.getCurrentAssignedSslCert()
+ }
+ this.enableSslAddDeleteButtons()
+ },
+ errorMessage: this.$t('message.assign.sslcert.failed'),
+ errorMethod: () => {
+ },
+ loadingMessage: this.$t('message.assign.sslcert.processing'),
+ catchMessage: this.$t('error.fetching.async.job.result'),
+ catchMethod: (e) => {
+ this.closeModal()
+ }
+ })
+ }).catch(error => {
+ this.$notifyError(error)
+ }).finally(() => {
+ })
+ },
+ removeSslFromLbRule () {
+ this.disableSslAddDeleteButtons()
+ getAPI('removeCertFromLoadBalancer', {
+ lbruleid: this.selectedRule.id
+ }).then(response => {
+ this.$pollJob({
+ jobId: response.removecertfromloadbalancerresponse.jobid,
+ successMessage: this.$t('message.success.remove.sslcert'),
+ successMethod: () => {
+ this.visible = true
+ this.getCurrentAssignedSslCert()
+ this.enableSslAddDeleteButtons()
+ },
+ errorMessage: this.$t('message.remove.sslcert.failed'),
+ errorMethod: () => {
+ this.visible = true
+ },
+ loadingMessage: this.$t('message.remove.sslcert.processing'),
+ catchMessage: this.$t('error.fetching.async.job.result'),
+ catchMethod: () => {
+ this.closeModal()
+ }
+ })
+ }).catch(error => {
+ this.$notifyError(error)
+ }).finally(() => {
+ })
+ },
+ enableSslAddDeleteButtons () {
+ this.deleteSslButtonVisible = true
+ this.addSslButtonVisible = true
+ },
+ disableSslAddDeleteButtons () {
+ this.addSslButtonVisible = false
+ this.deleteSslButtonVisible = false
+ },
+ handleOpenAddSslCertModal (record) {
+ this.addSslCertModalVisible = true
+ if (record) {
+ this.showAssignedSsl = true
+ this.addSslButtonVisible = true
+ this.selectedSsl = {}
+ } else {
+ this.showAssignedSsl = false
+ this.addSslButtonVisible = false
+ }
+ this.fetchSslCerts()
+ },
returnAlgorithmName (name) {
switch (name) {
case 'leastconn':
@@ -1705,6 +1932,9 @@
successMessage: this.$t('message.success.assign.vm'),
successMethod: () => {
this.parentToggleLoading()
+ if (this.newRule.protocol === 'ssl' && this.selectedSsl.id !== null) {
+ this.handleAddSslCert(data)
+ }
this.fetchData()
this.closeModal()
},
@@ -1780,6 +2010,7 @@
this.addNetworkModalLoading = false
this.addNetworkModalVisible = false
this.selectedTierForAutoScaling = null
+ this.addSslCertModalVisible = null
},
handleChangePage (page, pageSize) {
this.page = page