From 88cf9c3234c509d14cd502ec5a45c7b55605fd09 Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Tue, 19 Aug 2025 10:21:08 +0200 Subject: [PATCH 01/15] SSL offloading with Virtual Router --- .../cloud/agent/api/to/LoadBalancerTO.java | 4 + .../network/lb/LoadBalancingRulesService.java | 2 +- .../AssignCertToLoadBalancerCmd.java | 24 +- .../CreateLoadBalancerRuleCmd.java | 5 +- .../RemoveCertFromLoadBalancerCmd.java | 11 + .../facade/LoadBalancerConfigItem.java | 2 + .../model/LoadBalancerRule.java | 56 +++ .../cloud/network/HAProxyConfigurator.java | 136 ++++-- .../network/LoadBalancerConfigurator.java | 3 + .../network/HAProxyConfiguratorTest.java | 4 +- .../network/element/VirtualRouterElement.java | 4 +- .../lb/LoadBalancingRulesManagerImpl.java | 27 +- .../network/router/CommandSetupHelper.java | 1 + .../network/router/NetworkHelperImpl.java | 5 + .../VirtualNetworkApplianceManagerImpl.java | 10 +- .../network/ssl/CertServiceImpl.java | 97 +++-- .../network/ssl/CertServiceTest.java | 296 ++++++++++++- .../debian/opt/cloud/bin/cs/CsLoadBalancer.py | 36 ++ test/integration/smoke/test_ssl_offloading.py | 396 ++++++++++++++++++ tools/marvin/marvin/cloudstackConnection.py | 3 +- tools/marvin/marvin/config/test_data.py | 2 +- tools/marvin/marvin/lib/base.py | 76 ++++ ui/public/locales/en.json | 15 + ui/src/config/section/account.js | 2 +- ui/src/config/section/project.js | 4 + .../views/compute/AutoScaleLoadBalancing.vue | 1 + ui/src/views/iam/SSLCertificateTab.vue | 174 +++++++- ui/src/views/network/LoadBalancing.vue | 210 ++++++++++ 28 files changed, 1514 insertions(+), 92 deletions(-) create mode 100644 test/integration/smoke/test_ssl_offloading.py 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 f395f26aeed6..75646a0b5ed6 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 @@ -205,6 +205,10 @@ public LbSslCert getSslCert() { 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 46f17237e029..3fc6028b9771 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 @@ LoadBalancer createPublicLoadBalancerRule(String xId, String name, String descri 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 4f9d2f37d13f..bfc155468404 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignCertToLoadBalancerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/AssignCertToLoadBalancerCmd.java @@ -27,6 +27,7 @@ import org.apache.cloudstack.api.response.FirewallRuleResponse; import org.apache.cloudstack.api.response.SslCertResponse; import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.commons.lang3.BooleanUtils; import com.cloud.event.EventTypes; import com.cloud.exception.ConcurrentOperationException; @@ -57,11 +58,17 @@ 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 Long getCertId() { 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 34798c4efe1c..aa43b9cfdaf1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLoadBalancerRuleCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/CreateLoadBalancerRuleCmd.java @@ -33,6 +33,7 @@ import org.apache.cloudstack.api.response.NetworkResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.commons.lang3.StringUtils; import com.cloud.dc.DataCenter; import com.cloud.dc.DataCenter.NetworkType; @@ -112,7 +113,7 @@ 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 List getSourceCidrList() { } 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 dfaafe89923b..ddd2133d932e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/RemoveCertFromLoadBalancerCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/loadbalancer/RemoveCertFromLoadBalancerCmd.java @@ -82,4 +82,15 @@ public long getEntityOwnerId() { 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 4832c906699e..6dae886b4137 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 List generateConfig(final NetworkElementCommand cmd) { 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 3743d608e6c2..361c4765cc52 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 String getRouterIp() { 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 e4b0a7ffff4c..f37624cafed8 100644 --- a/core/src/main/java/com/cloud/network/HAProxyConfigurator.java +++ b/core/src/main/java/com/cloud/network/HAProxyConfigurator.java @@ -36,6 +36,8 @@ import com.cloud.agent.api.to.LoadBalancerTO.DestinationTO; import com.cloud.agent.api.to.LoadBalancerTO.StickinessPolicyTO; import com.cloud.agent.api.to.PortForwardingRuleTO; +import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRule.SslCertEntry; +import com.cloud.network.lb.LoadBalancingRule.LbSslCert; import com.cloud.network.rules.LbStickinessMethod.StickinessMethodType; import com.cloud.utils.Pair; import com.cloud.utils.net.NetUtils; @@ -52,6 +54,12 @@ 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 @@ private String getLbSubRuleForStickiness(final LoadBalancerTO lbTO) { 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 @@ private List getRulesForPool(final LoadBalancerTO lbTO, final boolean ke } 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 @@ private List getRulesForPool(final LoadBalancerTO lbTO, final boolean ke 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 @@ private List getRulesForPool(final LoadBalancerTO lbTO, final boolean ke if (StickinessMethodType.LBCookieBased.getName().equalsIgnoreCase(stickinessPolicy.getMethodName()) || StickinessMethodType.AppCookieBased.getName().equalsIgnoreCase(stickinessPolicy.getMethodName())) { httpbasedStickiness = true; + break; } } if (httpbasedStickiness) { - result.addAll(dstWithCookieSubRule); + backendConfigs.addAll(dstWithCookieSubRule); } else { - result.addAll(dstSubRule); + backendConfigs.addAll(dstSubRule); } - result.add(stickinessSubRule); + backendConfigs.add(stickinessSubRule); } else { - result.addAll(dstSubRule); + backendConfigs.addAll(dstSubRule); } if (stickinessSubRule != null && !destsAvailable) { logger.warn("Haproxy stickiness policy for lb rule: " + lbTO.getSrcIp() + ":" + lbTO.getSrcPort() + ": Not Applied, cause: backends are unavailable"); } - if (publicPort == NetUtils.HTTP_PORT && !keepAliveEnabled || httpbasedStickiness) { - sb = new StringBuilder(); - sb.append("\t").append("mode http"); - result.add(sb.toString()); - sb = new StringBuilder(); - sb.append("\t").append("option httpclose"); - result.add(sb.toString()); + boolean keepAliveEnabled = lbCmd.keepAliveEnabled; + boolean http = (publicPort == NetUtils.HTTP_PORT && !keepAliveEnabled); + if (http || httpbasedStickiness || sslOffloading) { + frontendConfigs.add("\tmode http"); + String keepAliveLine = keepAliveEnabled ? "\tno option forceclose" : "\toption httpclose"; + frontendConfigs.add(keepAliveLine); } + // add line like this: "listen 65_37_141_30-80\n\tbind 65.37.141.30:80" + result.add(String.format("listen %s", poolName)); + result.addAll(frontendConfigs); + String cidrList = lbTO.getCidrList(); if (StringUtils.isNotBlank(cidrList)) { result.add(String.format("\tacl network_allowed src %s \n\ttcp-request connection reject if !network_allowed", cidrList)); } + result.addAll(backendConfigs); result.add(blankLine); return result; } @@ -566,15 +595,18 @@ private List getRulesForPool(final LoadBalancerTO lbTO, final boolean ke 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 String[] generateConfiguration(final LoadBalancerConfigCommand lbCmd) { 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,26 @@ public String[][] generateFwRules(final LoadBalancerConfigCommand lbCmd) { return result; } + + @Override + public SslCertEntry[] generateSslCertEntries(LoadBalancerConfigCommand lbCmd) { + final Set sslCertEntries = new HashSet(); + for (final LoadBalancerTO lbTO : lbCmd.getLoadBalancers()) { + if (lbTO.getSslCert() != null) { + final LbSslCert cert = lbTO.getSslCert(); + if (cert.isRevoked()) { + continue; + } + if (lbTO.getLbProtocol() == null || ! lbTO.getLbProtocol().equals(NetUtils.SSL_PROTO)) { + continue; + } + 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); + } + } + final SslCertEntry[] result = sslCertEntries.toArray(new SslCertEntry[sslCertEntries.size()]); + return result; + } } diff --git a/core/src/main/java/com/cloud/network/LoadBalancerConfigurator.java b/core/src/main/java/com/cloud/network/LoadBalancerConfigurator.java index 0e19b1e606e9..8814f60b0714 100644 --- a/core/src/main/java/com/cloud/network/LoadBalancerConfigurator.java +++ b/core/src/main/java/com/cloud/network/LoadBalancerConfigurator.java @@ -23,6 +23,7 @@ import com.cloud.agent.api.routing.LoadBalancerConfigCommand; import com.cloud.agent.api.to.PortForwardingRuleTO; +import com.cloud.agent.resource.virtualnetwork.model.LoadBalancerRule.SslCertEntry; public interface LoadBalancerConfigurator { public final static int ADD = 0; @@ -34,4 +35,6 @@ public 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/network/HAProxyConfiguratorTest.java b/core/src/test/java/com/cloud/network/HAProxyConfiguratorTest.java index 2a282cbeca8b..13ba9d51e8bd 100644 --- a/core/src/test/java/com/cloud/network/HAProxyConfiguratorTest.java +++ b/core/src/test/java/com/cloud/network/HAProxyConfiguratorTest.java @@ -80,11 +80,11 @@ public void testGenerateConfigurationLoadBalancerConfigCommand() { 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 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 430d4757944c..0978fcba51b5 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 @@ private static Map> setCapabilities() { 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 ee4fe62aef9d..c031c7f745e3 100644 --- a/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java +++ b/server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java @@ -1267,7 +1267,7 @@ public LbSslCert getLbSslCert(long lbRuleId) { @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)); @@ -1294,8 +1294,13 @@ public boolean assignCertToLoadBalancer(long lbRuleId, Long certId) { //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"); + 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); + } //check for correct port if (loadBalancer.getLbProtocol() == null || !(loadBalancer.getLbProtocol().equals(NetUtils.SSL_PROTO))) @@ -2257,12 +2262,21 @@ public LoadBalancer updateLoadBalancerRule(UpdateLoadBalancerRuleCmd cmd) { 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)) { + LoadBalancerCertMapVO loadBalancerCertMapVO = _lbCertMapDao.findByLbRuleId(lbRuleId); + if (loadBalancerCertMapVO != null) { + logger.debug("Removing SSL cert for load balancer %s as the new protocol is not ssl but %s", lbRuleId, lb.getLbProtocol()); + _lbCertMapDao.remove(loadBalancerCertMapVO.getId()); + } + } } catch (ResourceUnavailableException e) { if (isRollBackAllowedForProvider(lb)) { /* @@ -2279,6 +2293,9 @@ public LoadBalancer updateLoadBalancerRule(UpdateLoadBalancerRuleCmd cmd) { 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); 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 10da04d04ca6..278c2531411b 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 void createApplyLoadBalancingRulesCommands(final List 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 f87e14c3561e..57537f0c907a 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,11 @@ public boolean validateHAProxyLBRule(final LoadBalancingRule rule) { return false; } + List lbProtocols = Arrays.asList("tcp", "udp", "tcp-proxy", "ssl"); + if (rule.getLbProtocol() != null && !lbProtocols.contains(rule.getLbProtocol())) { + throw new InvalidParameterValueException("protocol " + rule.getLbProtocol() + " is not in valid protocols " + lbProtocols); + } + for (final LoadBalancingRule.LbStickinessPolicy stickinessPolicy : rule.getStickinessPolicies()) { final List> paramsList = stickinessPolicy.getParams(); 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 8e4861273648..9c201fac2e5a 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,19 @@ private void updateWithLbRules(final DomainRouterJoinVO routerJoinVO, final Stri .append(",sourcePortEnd=").append(firewallRuleVO.getSourcePortEnd()); if (firewallRuleVO instanceof LoadBalancerVO) { LoadBalancerVO loadBalancerVO = (LoadBalancerVO) firewallRuleVO; - loadBalancingData.append(",sourceIp=").append(_ipAddressDao.findById(loadBalancerVO.getSourceIpAddressId()).getAddress().toString()) + String sourceIp = _ipAddressDao.findById(loadBalancerVO.getSourceIpAddressId()).getAddress().toString(); + loadBalancingData.append(",sourceIp=").append(sourceIp) .append(",destPortStart=").append(loadBalancerVO.getDefaultPortStart()) .append(",destPortEnd=").append(loadBalancerVO.getDefaultPortEnd()) .append(",algorithm=").append(loadBalancerVO.getAlgorithm()) .append(",protocol=").append(loadBalancerVO.getLbProtocol()); + if (loadBalancerVO.getLbProtocol() != null && loadBalancerVO.getLbProtocol().equals(NetUtils.SSL_PROTO)) { + final LbSslCert sslCert = _lbMgr.getLbSslCert(firewallRuleVO.getId()); + if (sslCert != null && ! sslCert.isRevoked()) { + loadBalancingData.append(",sslcert=").append(sourceIp.replace(".", "_")).append('-') + .append(loadBalancerVO.getSourcePortStart()).append(".pem"); + } + } } else if (firewallRuleVO instanceof ApplicationLoadBalancerRuleVO) { ApplicationLoadBalancerRuleVO appLoadBalancerVO = (ApplicationLoadBalancerRuleVO) firewallRuleVO; loadBalancingData.append(",sourceIp=").append(appLoadBalancerVO.getSourceIp()) 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 928e58a4f25c..6bba2a26b0f6 100644 --- a/server/src/main/java/org/apache/cloudstack/network/ssl/CertServiceImpl.java +++ b/server/src/main/java/org/apache/cloudstack/network/ssl/CertServiceImpl.java @@ -26,8 +26,9 @@ import java.security.NoSuchProviderException; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.SecureRandom; import java.security.Security; +import java.security.Signature; +import java.security.SignatureException; import java.security.cert.CertPathBuilder; import java.security.cert.CertPathBuilderException; import java.security.cert.CertStore; @@ -48,10 +49,6 @@ import java.util.List; import java.util.Set; -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; import javax.inject.Inject; import org.apache.cloudstack.acl.SecurityChecker; @@ -62,9 +59,21 @@ import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.network.tls.CertService; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMEncryptedKeyPair; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder; +import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; +import org.bouncycastle.operator.InputDecryptorProvider; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; +import org.bouncycastle.pkcs.PKCSException; import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemReader; @@ -89,7 +98,6 @@ import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.security.CertificateHelper; import com.google.common.base.Preconditions; -import org.apache.commons.lang3.StringUtils; public class CertServiceImpl implements CertService { @@ -279,11 +287,11 @@ public List listSslCerts(final ListSslCertsCmd listSslCertCmd) 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 @@ private void validate(final String certInput, final String keyInput, final Strin 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 @@ private void validateKeys(final PublicKey pubKey, final PrivateKey privKey) { 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()); - - cipher.init(Cipher.DECRYPT_MODE, pubKey, random); - final String decreptedData = new String(cipher.doFinal(encryptedData)); - if (!decreptedData.equals(data)) { + Signature sig = Signature.getInstance("SHA256withRSA"); + sig.initSign(privKey); + sig.update(data.getBytes()); + byte[] signature = sig.sign(); + + 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,47 @@ private void validateChain(final List chain, final Certificate cert } - 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) { + 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); + } else if (privateKeyObj instanceof PEMEncryptedKeyPair) { + if (password == null) { + throw new CloudRuntimeException("Key is encrypted but password is null"); + } + PEMEncryptedKeyPair encryptedKeyPair = (PEMEncryptedKeyPair)privateKeyObj; + privateKey = new JcaPEMKeyConverter().getKeyPair( + encryptedKeyPair.decryptKeyPair(new JcePEMDecryptorProviderBuilder().build(password.toCharArray()))).getPrivate(); + } 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; } @Override 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 5a2f12ff524e..0685167c2a42 100644 --- a/server/src/test/java/org/apache/cloudstack/network/ssl/CertServiceTest.java +++ b/server/src/test/java/org/apache/cloudstack/network/ssl/CertServiceTest.java @@ -34,6 +34,13 @@ import org.apache.cloudstack.api.command.user.loadbalancer.DeleteSslCertCmd; import org.apache.cloudstack.api.command.user.loadbalancer.UploadSslCertCmd; import org.apache.cloudstack.context.CallContext; +import org.bouncycastle.openssl.PKCS8Generator; +import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8EncryptorBuilder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.OutputEncryptor; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; import org.junit.After; import org.junit.Assert; import org.junit.Assume; @@ -44,9 +51,13 @@ import java.io.File; import java.io.IOException; +import java.io.StringWriter; import java.lang.reflect.Field; import java.net.URLDecoder; import java.nio.charset.Charset; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -207,7 +218,7 @@ public void runUploadSslCertWithNoRevocationInfo() throws Exception { } } - // @Test + @Test /** * Given a Self-signed Certificate with encrypted key, upload should succeed */ @@ -456,7 +467,7 @@ public void runUploadSslCertBadPassword() throws IOException, IllegalAccessExcep 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 void runUploadSslCertBadkeyAlgo() throws IOException, IllegalAccessExcept 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 long getEntityOwnerId() { 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 a92f06b18701..39de9b55d158 100755 --- a/systemvm/debian/opt/cloud/bin/cs/CsLoadBalancer.py +++ b/systemvm/debian/opt/cloud/bin/cs/CsLoadBalancer.py @@ -16,6 +16,7 @@ # under the License. import logging import os.path +from os import listdir import re from cs.CsDatabag import CsDataBag from .CsProcess import CsProcess @@ -25,6 +26,7 @@ HAPROXY_CONF_T = "/etc/haproxy/haproxy.cfg.new" HAPROXY_CONF_P = "/etc/haproxy/haproxy.cfg" +SSL_CERTS_DIR = "/etc/cloudstack/ssl/" class CsLoadBalancer(CsDataBag): """ Manage Load Balancer entries """ @@ -34,6 +36,9 @@ def process(self): 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 @@ def process(self): 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 @@ def _configure_firewall(self, add_rules, remove_rules, stat_rules): 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 000000000000..15c598e465af --- /dev/null +++ b/test/integration/smoke/test_ssl_offloading.py @@ -0,0 +1,396 @@ +# 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.cloudstackAPI import (uploadSslCert, + deleteSslCert) +from marvin.lib.utils import wait_until +from marvin.lib.base import (Account, + UserData, + SslCertificate, + Template, + NetworkOffering, + ServiceOffering, + VirtualMachine, + Network, + PublicIPAddress, + LoadBalancerRule) +from marvin.lib.common import (get_domain, get_zone, get_test_template) +from nose.plugins.attrib import attr + +import os +import subprocess + + +_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 +} + +USER_DATA="""I2Nsb3VkLWNvbmZpZwoKcnVuY21kOgogIC0gc3VkbyBhcHQtZ2V0IHVwZGF0Z +QogIC0gc3VkbyBhcHQtZ2V0IGluc3RhbGwgLXkgYXBhY2hlMgogIC0gc3Vkby +BzeXN0ZW1jdGwgZW5hYmxlIGFwYWNoZTIKICAtIHN1ZG8gc3lzdGVtY3RsIHN0 +YXJ0IGFwYWNoZTIKICAtIGVjaG8gIlRlc3QgcGFnZSIgfHN1ZG8gdGVlIC92YX +Ivd3d3L2h0bWwvaW5kZXguaHRtbAoKCg==""" + +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 + + #Create an account, network, VM and IP addresses + cls.account = Account.create( + cls.apiclient, + cls.services["account"], + admin=True, + domainid=cls.domain.id + ) + # Register Userdata + cls.userdata = UserData.register(cls.apiclient, + name="test-userdata", + userdata=USER_DATA, + account=cls.account.name, + domainid=cls.account.domainid + ) + + # Upload SSL Certificate, save chain as a file + cls.sslcert = SslCertificate.create(cls.apiclient, + CERT, + name="test-ssl-certificate", + account=cls.account.name, + domainid=cls.account.domainid) + + 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 + ) + + # 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) + cls._cleanup.append(cls.service_offering) + cls._cleanup.append(cls.account) + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.cleanup = [] + + def tearDown(self): + super(TestSslOffloading, self).tearDown() + if os.path.exists(FULL_CHAIN): + os.remove(FULL_CHAIN) + + @classmethod + def tearDownClass(cls): + super(TestSslOffloading, cls).tearDownClass() + + def wait_for_service_ready(self, command, expected, retries=60): + output = None + def check_output(): + try: + output = subprocess.check_output(command, shell=True).strip().decode('utf-8') + except Exception as e: + print("Failed to get output of command %s: %s" % (command, e)) + if expected is None: + print("But it is expected") + return True, None + return False, None + print("Output of command %s: \n %s" %(command, output)) + if expected is None: + print("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)) + return res + + @attr(tags = ["advanced", "advancedns", "smoke"], required_hardware="true") + def test_01_ssl_offloading(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 + + # 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.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.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 -L --connect-timeout 3 http://%s/" % 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/" % (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/" % (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/" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN) + self.wait_for_service_ready(command, "SSL certificate problem", 1) + + command = "curl -L --connect-timeout 3 -k --resolve %s:443:%s https://%s/" % (DOMAIN, self.public_ip.ipaddress.ipaddress, DOMAIN) + self.wait_for_service_ready(command, CONTENT, 1) + + command = "curl -L --connect-timeout 3 --cacert %s --resolve %s:443:%s https://%s/" % (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) diff --git a/tools/marvin/marvin/cloudstackConnection.py b/tools/marvin/marvin/cloudstackConnection.py index 5b438daceb72..d64046b7f97c 100644 --- a/tools/marvin/marvin/cloudstackConnection.py +++ b/tools/marvin/marvin/cloudstackConnection.py @@ -164,9 +164,10 @@ def __sendPostReqToCS(self, url, payload): ''' 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 edacf163db4d..e3d4022cf0f9 100644 --- a/tools/marvin/marvin/config/test_data.py +++ b/tools/marvin/marvin/config/test_data.py @@ -1068,7 +1068,7 @@ "displaytext": "ubuntu 22.04 kvm", "format": "raw", "hypervisor": "kvm", - "ostype": "Other Linux (64-bit)", + "ostype": "Ubuntu 22.04 LTS", "url": "https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-amd64.img", "requireshvm": "True", "ispublic": "True", diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 16b2467b63df..ac6e97a8119e 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -3080,6 +3080,9 @@ def create(cls, apiclient, services, ipaddressid=None, accountid=None, 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 @@ def listLoadBalancerRuleInstances(cls, apiclient, id, lbvmips=False, applied=Non [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 @@ def update(self, apiclient, **kwargs): 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 394de6ca6d26..453c75cf55cc 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 evocation 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", @@ -2054,6 +2058,7 @@ "label.removed": "Removed", "label.removing": "Removing", "label.replace.acl": "Replace ACL", +"label.replace.ssl.cert": "Replace SSL certificate", "label.report.bug": "Ask a question or Report an issue", "label.request": "Request", "label.required": "Required", @@ -2327,6 +2332,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 +2594,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 +3004,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 +3057,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 +3774,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 +3853,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 +3902,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 55b950d39017..5766fd8a0f92 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 18354c3c7ec9..5a1f5f71c81d 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 a24e92826183..6c04ce1c2504 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 e5890ac13370..28c023ab9a9b 100644 --- a/ui/src/views/iam/SSLCertificateTab.vue +++ b/ui/src/views/iam/SSLCertificateTab.vue @@ -17,6 +17,17 @@