diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 32b70145e59..5c79d1dd002 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,6 +48,7 @@ repos: 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 @@ repos: ^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 @@ repos: 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 f395f26aeed..6c4b9e607c5 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 @@ public class LoadBalancerTO { 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 @@ public class LoadBalancerTO { 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 46f17237e02..3fc6028b977 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 @@ public interface LoadBalancingRulesService { 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 4f9d2f37d13..bfc15546840 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.ServerApiException; 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 @@ public class AssignCertToLoadBalancerCmd extends BaseAsyncCmd { 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 class AssignCertToLoadBalancerCmd extends BaseAsyncCmd { 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 34798c4efe1..aa43b9cfdaf 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.LoadBalancerResponse; 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 @@ public class CreateLoadBalancerRuleCmd extends BaseAsyncCreateCmd /*implements L + "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 class CreateLoadBalancerRuleCmd extends BaseAsyncCreateCmd /*implements L } 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 dfaafe89923..ddd2133d932 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 class RemoveCertFromLoadBalancerCmd extends BaseAsyncCmd { 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 4832c906699..6dae886b413 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 @@ public class LoadBalancerConfigItem extends AbstractConfigItemFacade { 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 rules = new LinkedList(); 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 3743d608e6c..361c4765cc5 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 @@ public class LoadBalancerRule { private String[] configuration; private String tmpCfgFilePath; private String tmpCfgFileName; + private SslCertEntry[] sslCerts; private String[] addRules; private String[] removeRules; @@ -32,6 +33,53 @@ public class LoadBalancerRule { 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 class LoadBalancerRule { 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 e4b0a7ffff4..7736bea3cda 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; 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 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator { 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 fwRules) { // Group the rules by publicip:publicport @@ -469,30 +477,41 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator { return sb.toString(); } - private List getRulesForPool(final LoadBalancerTO lbTO, final boolean keepAliveEnabled) { + private List 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 result = new ArrayList(); - // 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 frontendConfigs = new ArrayList<>(); + final List backendConfigs = new ArrayList<>(); + final List 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 dstSubRule = new ArrayList(); - final List dstWithCookieSubRule = new ArrayList(); + final List dstSubRule = new ArrayList<>(); + final List 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 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator { } 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 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator { 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 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator { 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 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator { 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 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator { if (lbTO.isRevoked()) { continue; } - final List poolRules = getRulesForPool(lbTO, lbCmd.keepAliveEnabled); + final List poolRules = getRulesForPool(lbTO, lbCmd); result.addAll(poolRules); has_listener = true; } @@ -696,4 +728,30 @@ public class HAProxyConfigurator implements LoadBalancerConfigurator { return result; } + + @Override + public SslCertEntry[] generateSslCertEntries(LoadBalancerConfigCommand lbCmd) { + final Set 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 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 0e19b1e606e..8814f60b071 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 java.util.List; 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 interface LoadBalancerConfigurator { 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 042bec9d216..6d4c6234c42 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.IpAssociation; 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 @@ public class ConfigHelperTest { protected LoadBalancerConfigCommand generateLoadBalancerConfigCommand() { final List lbs = new ArrayList<>(); final List 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 00000000000..f2b25a5f78e --- /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 2a282cbeca8..72361c2880e 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 org.junit.Test; 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 @@ public class HAProxyConfiguratorTest { 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 @@ public class HAProxyConfiguratorTest { 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 80bb44a1f4a..1e73cd7b33e 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 @@ import com.cloud.utils.db.GenericDao; public interface SslCertDao extends GenericDao { List 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 185c18aecd8..efadc009dfc 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 @@ public class SslCertDaoImpl extends GenericDaoBase implements S return listBy(sc); } + @Override + public int removeByAccountId(long accountId) { + SearchCriteria 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 430d4757944..0978fcba51b 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 @@ NetworkMigrationResponder, AggregatedCommandExecutor, RedundantResource, DnsServ final Map lbCapabilities = new HashMap(); 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 ee4fe62aef9..f786626ee31 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 @@ public class LoadBalancingRulesManagerImpl extends ManagerBase implements @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 @@ public class LoadBalancingRulesManagerImpl extends ManagerBase implements 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 @@ public class LoadBalancingRulesManagerImpl extends ManagerBase implements 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 @@ public class LoadBalancingRulesManagerImpl extends ManagerBase implements return handled; } - private LoadBalancingRule getLoadBalancerRuleToApply(LoadBalancerVO lb) { + protected LoadBalancingRule getLoadBalancerRuleToApply(LoadBalancerVO lb) { List policyList = getStickinessPolicies(lb.getId()); Ip sourceIp = getSourceIp(lb); @@ -2257,12 +2266,17 @@ public class LoadBalancingRulesManagerImpl extends ManagerBase implements 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 @@ public class LoadBalancingRulesManagerImpl extends ManagerBase implements 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 @@ public class LoadBalancingRulesManagerImpl extends ManagerBase implements } } + 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> 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 10da04d04ca..278c2531411 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 @@ public class CommandSetupHelper { 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 f87e14c3561..d026bd914a3 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 @@ public class NetworkHelperImpl implements NetworkHelper { return false; } + validateHAproxyLbProtocol(rule.getLbProtocol()); + for (final LoadBalancingRule.LbStickinessPolicy stickinessPolicy : rule.getStickinessPolicies()) { final List> paramsList = stickinessPolicy.getParams(); @@ -982,6 +984,13 @@ public class NetworkHelperImpl implements NetworkHelper { return true; } + private void validateHAproxyLbProtocol(String lbProtocol) { + List 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 8e486127364..19cec194345 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 @@ Configurable, StateListener getRouterHealthChecksConfig(final DomainRouterVO router) { Map data = new HashMap<>(); List 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 a73a00d9152..b5da605f328 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.NetworkDao; 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 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M private UserDataDao userDataDao; @Inject private NetworkPermissionDao networkPermissionDao; + @Inject + private SslCertDao sslCertDao; private List _querySelectors; @@ -1203,6 +1206,9 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M // 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 928e58a4f25..d101ab989ca 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.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.SecureRandom; import java.security.Security; +import java.security.Signature; +import java.security.SignatureException; import java.security.cert.CertPathBuilder; import java.security.cert.CertPathBuilderException; import java.security.cert.CertStore; @@ -48,10 +49,6 @@ import java.util.HashSet; 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.api.response.SslCertResponse; 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.db.EntityManager; 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 @@ public class CertServiceImpl implements CertService { 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 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 @@ public class CertServiceImpl implements CertService { 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 @@ public class CertServiceImpl implements CertService { 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 class CertServiceImpl implements CertService { } - 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 184c853637b..78655ba9a05 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.Mock; 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 @@ public class LoadBalancingRulesManagerImplTest{ @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 @@ public class LoadBalancingRulesManagerImplTest{ List 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 @@ public class LoadBalancingRulesManagerImplTest{ 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 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 cadbc3eb966..0365ae20175 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.FirewallRulesDao; 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.Site2SiteVpnGatewayDao; 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.storage.dao.VolumeDao; 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 @@ public class VirtualNetworkApplianceManagerImplTest { @Mock private BGPService bgpService; + @Mock + private LoadBalancingRulesManager _lbMgr; + // @InjectMocks // private VirtualNetworkApplianceManagerImpl virtualNetworkApplianceManagerImpl; @@ -391,4 +398,21 @@ public class VirtualNetworkApplianceManagerImplTest { 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 3055e48247c..846d8cdc989 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 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { 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 98f152088ed..90e27790966 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.AccountGuestVlanMapDao; 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 @@ public class AccountManagetImplTestBase { @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 5a2f12ff524..0685167c2a4 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 com.cloud.utils.db.TransactionLegacy; 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 org.mockito.Mockito; 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 @@ public class CertServiceTest { } } - // @Test + @Test /** * Given a Self-signed Certificate with encrypted key, upload should succeed */ @@ -456,7 +467,7 @@ public class CertServiceTest { 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 @@ public class CertServiceTest { 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 @@ public class CertServiceTest { 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 a92f06b1870..39de9b55d15 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 @@ from . import CsHelper 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 @@ class CsLoadBalancer(CsDataBag): 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 @@ class CsLoadBalancer(CsDataBag): 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 @@ class CsLoadBalancer(CsDataBag): 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 00000000000..5f0ea9adc54 --- /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 5b438daceb7..d64046b7f97 100644 --- a/tools/marvin/marvin/cloudstackConnection.py +++ b/tools/marvin/marvin/cloudstackConnection.py @@ -164,9 +164,10 @@ class CSConnection(object): ''' 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 edacf163db4..e3d4022cf0f 100644 --- a/tools/marvin/marvin/config/test_data.py +++ b/tools/marvin/marvin/config/test_data.py @@ -1068,7 +1068,7 @@ test_data = { "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 16b2467b63d..ac6e97a8119 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -3080,6 +3080,9 @@ class LoadBalancerRule: 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 @@ class LoadBalancerRule: [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 @@ class GpuDevice: 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 394de6ca6d2..5136f241fb3 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: ", @@ -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.
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.
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 55b950d3901..5766fd8a0f9 100644 --- a/ui/src/config/section/account.js +++ b/ui/src/config/section/account.js @@ -85,7 +85,7 @@ export default { 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 18354c3c7ec..5a1f5f71c81 100644 --- a/ui/src/config/section/project.js +++ b/ui/src/config/section/project.js @@ -46,6 +46,10 @@ export default { 'listProjectRoles' in store.getters.apis } }, + { + 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 a24e9282618..6c04ce1c250 100644 --- a/ui/src/views/compute/AutoScaleLoadBalancing.vue +++ b/ui/src/views/compute/AutoScaleLoadBalancing.vue @@ -284,6 +284,7 @@ {{ $t('label.tcp.proxy') }} {{ $t('label.tcp') }} {{ $t('label.udp') }} + {{ $t('label.ssl') }}
diff --git a/ui/src/views/iam/SSLCertificateTab.vue b/ui/src/views/iam/SSLCertificateTab.vue index e5890ac1337..08f1ee4b580 100644 --- a/ui/src/views/iam/SSLCertificateTab.vue +++ b/ui/src/views/iam/SSLCertificateTab.vue @@ -17,6 +17,17 @@