Improvement: SSL offloading with Virtual Router (#11468)

* SSL offloading with Virtual Router

* PR11468: fix pre-commit errors

* PR11468: api->getAPI/postAPI in UI

* SSL: add smoke tests for VPC in user project

* PR11468: address Daan's comments

* Fix test/integration/smoke/test_ssl_offloading.py

* SSL: remove ssl certificates when clean up account

* SSL offloading: add unit tests

* SSL offloading: UI fixes part 1

* SSL offloading: UI changes part 2

* SSL offloading: add more unit tests

* SSL offloading: more unit tests 3

* SSL offloading: wrong check

* SSL offloading: more and more unit tests

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