This is an automated email from the ASF dual-hosted git repository.

sdanilov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 1d3924acb6 IGNITE-18577: Add SSL support for the REST (#1632)
1d3924acb6 is described below

commit 1d3924acb6072519e202be3a82fe91bc893c0121
Author: Ivan Gagarkin <[email protected]>
AuthorDate: Wed Feb 8 14:54:21 2023 +0400

    IGNITE-18577: Add SSL support for the REST (#1632)
---
 .../org/apache/ignite/network/NodeMetadata.java    |  32 +++-
 .../internal/cli/commands/ItConfigCommandTest.java |  10 +-
 .../repl/registry/impl/NodeNameRegistryImpl.java   |   2 +-
 .../management/rest/TopologyController.java        |   2 +-
 .../network/scalecube/ItClusterServiceTest.java    |   4 +-
 .../KeyStoreConfigurationSchema.java}              |  27 ++--
 .../KeyStoreConfigurationValidator.java}           |  27 ++--
 .../KeyStoreConfigurationValidatorImpl.java        |  57 +++++++
 .../KeyStoreConfigurationValidatorImplTest.java    |  77 +++++++++
 .../network/configuration/StubKeyStoreView.java}   |  41 +++--
 .../internal/rest/api/cluster/NodeMetadataDto.java |  34 ++--
 modules/rest/build.gradle                          |   1 +
 modules/rest/openapi/openapi.yaml                  |   8 +-
 .../apache/ignite/internal/rest/RestComponent.java | 141 +++++++++++++----
 .../configuration/RestConfigurationModule.java     |   8 +
 .../configuration/RestConfigurationSchema.java     |  13 ++
 ...Schema.java => RestSslConfigurationSchema.java} |  31 ++--
 .../ignite/internal/rest/ItPortRangeTest.java      | 130 +++++++++++++++
 .../ignite/internal/rest/ssl/ItRestSslTest.java    | 175 +++++++++++++++++++++
 .../apache/ignite/internal/rest/ssl/RestNode.java  |  99 ++++++++++++
 .../src/integrationTest/resources/ssl/keystore.p12 | Bin 0 -> 4286 bytes
 .../integrationTest/resources/ssl/truststore.jks   | Bin 0 -> 1558 bytes
 .../org/apache/ignite/internal/app/IgniteImpl.java |  35 ++++-
 .../internal/component/RestAddressReporter.java    |  23 ++-
 .../component/RestAddressReporterTest.java         |  41 ++++-
 packaging/zip/ignite3db                            |   2 +-
 26 files changed, 889 insertions(+), 131 deletions(-)

diff --git 
a/modules/api/src/main/java/org/apache/ignite/network/NodeMetadata.java 
b/modules/api/src/main/java/org/apache/ignite/network/NodeMetadata.java
index 790ad4ba3c..154c602d2c 100644
--- a/modules/api/src/main/java/org/apache/ignite/network/NodeMetadata.java
+++ b/modules/api/src/main/java/org/apache/ignite/network/NodeMetadata.java
@@ -28,19 +28,27 @@ public class NodeMetadata implements Serializable {
 
     private final String restHost;
 
-    private final int restPort;
+    private final int httpPort;
 
-    public NodeMetadata(String restHost, int restPort) {
+    private final int httpsPort;
+
+    /** Constructor. */
+    public NodeMetadata(String restHost, int httpPort, int httpsPort) {
         this.restHost = restHost;
-        this.restPort = restPort;
+        this.httpPort = httpPort;
+        this.httpsPort = httpsPort;
     }
 
     public String restHost() {
         return restHost;
     }
 
-    public int restPort() {
-        return restPort;
+    public int httpPort() {
+        return httpPort;
+    }
+
+    public int httpsPort() {
+        return httpsPort;
     }
 
     @Override
@@ -48,15 +56,23 @@ public class NodeMetadata implements Serializable {
         if (this == o) {
             return true;
         }
-        if (!(o instanceof NodeMetadata)) {
+        if (o == null || getClass() != o.getClass()) {
             return false;
         }
+
         NodeMetadata that = (NodeMetadata) o;
-        return restPort == that.restPort && Objects.equals(restHost, 
that.restHost);
+
+        if (httpPort != that.httpPort) {
+            return false;
+        }
+        if (httpsPort != that.httpsPort) {
+            return false;
+        }
+        return restHost != null ? restHost.equals(that.restHost) : 
that.restHost == null;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(restHost, restPort);
+        return Objects.hash(restHost, httpPort, httpsPort);
     }
 }
diff --git 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/ItConfigCommandTest.java
 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/ItConfigCommandTest.java
index 024b027c4b..77fed750a4 100644
--- 
a/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/ItConfigCommandTest.java
+++ 
b/modules/cli/src/integrationTest/java/org/apache/ignite/internal/cli/commands/ItConfigCommandTest.java
@@ -73,7 +73,7 @@ public class ItConfigCommandTest extends AbstractCliTest {
                 "config",
                 "update",
                 "--node-url",
-                "http://localhost:"; + node.restAddress().port(),
+                "http://localhost:"; + node.restHttpAddress().port(),
                 "network.shutdownQuietPeriod=1"
         );
 
@@ -91,7 +91,7 @@ public class ItConfigCommandTest extends AbstractCliTest {
                 "config",
                 "show",
                 "--node-url",
-                "http://localhost:"; + node.restAddress().port()
+                "http://localhost:"; + node.restHttpAddress().port()
         );
 
         assertEquals(0, exitCode);
@@ -109,7 +109,7 @@ public class ItConfigCommandTest extends AbstractCliTest {
                 "config",
                 "update",
                 "--node-url",
-                "http://localhost:"; + node.restAddress().port(),
+                "http://localhost:"; + node.restHttpAddress().port(),
                 "network.foo=\"bar\""
         );
 
@@ -126,7 +126,7 @@ public class ItConfigCommandTest extends AbstractCliTest {
                 "config",
                 "update",
                 "--node-url",
-                "http://localhost:"; + node.restAddress().port(),
+                "http://localhost:"; + node.restHttpAddress().port(),
                 "network.shutdownQuietPeriod=asd"
         );
 
@@ -144,7 +144,7 @@ public class ItConfigCommandTest extends AbstractCliTest {
                 "config",
                 "show",
                 "--node-url",
-                "http://localhost:"; + node.restAddress().port(),
+                "http://localhost:"; + node.restHttpAddress().port(),
                 "network"
         );
 
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/registry/impl/NodeNameRegistryImpl.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/registry/impl/NodeNameRegistryImpl.java
index 72a048b13b..e0de828d6a 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/registry/impl/NodeNameRegistryImpl.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/repl/registry/impl/NodeNameRegistryImpl.java
@@ -94,7 +94,7 @@ public class NodeNameRegistryImpl implements 
NodeNameRegistry, AsyncSessionEvent
             return null;
         }
         try {
-            return new URL("http://"; + metadata.getRestHost() + ":" + 
metadata.getRestPort());
+            return new URL("http://"; + metadata.getRestHost() + ":" + 
metadata.getHttpPort());
         } catch (Exception e) {
             log.warn("Couldn't create URL: {}", e);
             return null;
diff --git 
a/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/TopologyController.java
 
b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/TopologyController.java
index 52688aafc2..e926f7ad3f 100644
--- 
a/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/TopologyController.java
+++ 
b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/TopologyController.java
@@ -87,6 +87,6 @@ public class TopologyController implements TopologyApi {
         if (metadata == null) {
             return null;
         }
-        return new NodeMetadataDto(metadata.restHost(), metadata.restPort());
+        return new NodeMetadataDto(metadata.restHost(), metadata.httpPort(), 
metadata.httpsPort());
     }
 }
diff --git 
a/modules/network/src/integrationTest/java/org/apache/ignite/network/scalecube/ItClusterServiceTest.java
 
b/modules/network/src/integrationTest/java/org/apache/ignite/network/scalecube/ItClusterServiceTest.java
index c6400f789c..089736c8e5 100644
--- 
a/modules/network/src/integrationTest/java/org/apache/ignite/network/scalecube/ItClusterServiceTest.java
+++ 
b/modules/network/src/integrationTest/java/org/apache/ignite/network/scalecube/ItClusterServiceTest.java
@@ -82,8 +82,8 @@ public class ItClusterServiceTest {
         assertTrue(waitForCondition(() -> 
service2.topologyService().allMembers().size() == 2, 1000));
         try {
             
assertThat(service1.topologyService().localMember().nodeMetadata(), 
is(nullValue()));
-            var meta1 = new NodeMetadata("foo", 123);
-            var meta2 = new NodeMetadata("bar", 456);
+            var meta1 = new NodeMetadata("foo", 123, 321);
+            var meta2 = new NodeMetadata("bar", 456, 654);
             service1.updateMetadata(meta1);
             service2.updateMetadata(meta2);
             checkLocalMeta(service1, meta1);
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
 
b/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchema.java
similarity index 59%
copy from 
modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
copy to 
modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchema.java
index cabd1c5a6b..895fa204d9 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
+++ 
b/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationSchema.java
@@ -15,26 +15,21 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.rest.configuration;
+package org.apache.ignite.internal.network.configuration;
 
-import org.apache.ignite.configuration.annotation.ConfigurationRoot;
-import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.configuration.annotation.Config;
 import org.apache.ignite.configuration.annotation.Value;
-import org.apache.ignite.configuration.validation.Range;
 
-/**
- * Configuration schema for REST endpoint subtree.
- */
-@SuppressWarnings("PMD.UnusedPrivateField")
-@ConfigurationRoot(rootName = "rest", type = ConfigurationType.LOCAL)
-public class RestConfigurationSchema {
-    /** TCP port. */
-    @Range(min = 1024, max = 0xFFFF)
+/** Key store configuration. */
+@Config
+public class KeyStoreConfigurationSchema {
+
+    @Value(hasDefault = true)
+    public String type = "PKCS12";
+
     @Value(hasDefault = true)
-    public final int port = 10300;
+    public String path = "";
 
-    /** TCP port range. */
-    @Range(min = 0)
     @Value(hasDefault = true)
-    public final int portRange = 100;
+    public String password = "";
 }
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
 
b/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidator.java
similarity index 52%
copy from 
modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
copy to 
modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidator.java
index cabd1c5a6b..e96b9bd2be 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
+++ 
b/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidator.java
@@ -15,26 +15,17 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.rest.configuration;
+package org.apache.ignite.internal.network.configuration;
 
-import org.apache.ignite.configuration.annotation.ConfigurationRoot;
-import org.apache.ignite.configuration.annotation.ConfigurationType;
-import org.apache.ignite.configuration.annotation.Value;
-import org.apache.ignite.configuration.validation.Range;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 
 /**
- * Configuration schema for REST endpoint subtree.
+ * Annotation to validate whole key store configuration.
  */
-@SuppressWarnings("PMD.UnusedPrivateField")
-@ConfigurationRoot(rootName = "rest", type = ConfigurationType.LOCAL)
-public class RestConfigurationSchema {
-    /** TCP port. */
-    @Range(min = 1024, max = 0xFFFF)
-    @Value(hasDefault = true)
-    public final int port = 10300;
-
-    /** TCP port range. */
-    @Range(min = 0)
-    @Value(hasDefault = true)
-    public final int portRange = 100;
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface KeyStoreConfigurationValidator {
 }
diff --git 
a/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidatorImpl.java
 
b/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidatorImpl.java
new file mode 100644
index 0000000000..61ffbc7c02
--- /dev/null
+++ 
b/modules/network/src/main/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidatorImpl.java
@@ -0,0 +1,57 @@
+/*
+ * 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 org.apache.ignite.internal.network.configuration;
+
+import static org.apache.ignite.internal.util.StringUtils.nullOrBlank;
+
+import org.apache.ignite.configuration.validation.ValidationContext;
+import org.apache.ignite.configuration.validation.ValidationIssue;
+import org.apache.ignite.configuration.validation.Validator;
+
+/**
+ * Key store configuration validator implementation.
+ */
+public class KeyStoreConfigurationValidatorImpl implements 
Validator<KeyStoreConfigurationValidator, KeyStoreView> {
+
+    public static final KeyStoreConfigurationValidatorImpl INSTANCE = new 
KeyStoreConfigurationValidatorImpl();
+
+    @Override
+    public void validate(KeyStoreConfigurationValidator annotation, 
ValidationContext<KeyStoreView> ctx) {
+        KeyStoreView keyStore = ctx.getNewValue();
+        String type = keyStore.type();
+        String path = keyStore.path();
+        String password = keyStore.password();
+        if (nullOrBlank(path) && nullOrBlank(password)) {
+            return;
+        } else {
+            if (nullOrBlank(type)) {
+                ctx.addIssue(new ValidationIssue(
+                        ctx.currentKey(),
+                        "Key store type must not be blank"
+                ));
+            }
+
+            if (nullOrBlank(path)) {
+                ctx.addIssue(new ValidationIssue(
+                        ctx.currentKey(),
+                        "Key store path must not be blank"
+                ));
+            }
+        }
+    }
+}
diff --git 
a/modules/network/src/test/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidatorImplTest.java
 
b/modules/network/src/test/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidatorImplTest.java
new file mode 100644
index 0000000000..9399660210
--- /dev/null
+++ 
b/modules/network/src/test/java/org/apache/ignite/internal/network/configuration/KeyStoreConfigurationValidatorImplTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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 org.apache.ignite.internal.network.configuration;
+
+import static 
org.apache.ignite.internal.configuration.validation.TestValidationUtil.mockValidationContext;
+import static 
org.apache.ignite.internal.configuration.validation.TestValidationUtil.validate;
+import static org.mockito.Mockito.mock;
+
+import org.apache.ignite.configuration.validation.ValidationContext;
+import org.junit.jupiter.api.Test;
+
+class KeyStoreConfigurationValidatorImplTest {
+
+    @Test
+    public void nullPath() {
+        ValidationContext<KeyStoreView> ctx = mockValidationContext(
+                null,
+                new StubKeyStoreView("PKCS12", null, "changeIt")
+        );
+        validate(KeyStoreConfigurationValidatorImpl.INSTANCE, 
mock(KeyStoreConfigurationValidator.class), ctx,
+                "Key store path must not be blank");
+    }
+
+    @Test
+    public void emptyPath() {
+        ValidationContext<KeyStoreView> ctx = mockValidationContext(
+                null,
+                new StubKeyStoreView("PKCS12", "", "changeIt")
+        );
+        validate(KeyStoreConfigurationValidatorImpl.INSTANCE, 
mock(KeyStoreConfigurationValidator.class), ctx,
+                "Key store path must not be blank");
+    }
+
+    @Test
+    public void nullType() {
+        ValidationContext<KeyStoreView> ctx = mockValidationContext(
+                null,
+                new StubKeyStoreView(null, "/path/to/keystore.p12", null)
+        );
+        validate(KeyStoreConfigurationValidatorImpl.INSTANCE, 
mock(KeyStoreConfigurationValidator.class), ctx,
+                "Key store type must not be blank");
+    }
+
+    @Test
+    public void emptyType() {
+        ValidationContext<KeyStoreView> ctx = mockValidationContext(
+                null,
+                new StubKeyStoreView("", "/path/to/keystore.p12", null)
+        );
+        validate(KeyStoreConfigurationValidatorImpl.INSTANCE, 
mock(KeyStoreConfigurationValidator.class), ctx,
+                "Key store type must not be blank");
+    }
+
+    @Test
+    public void validConfig() {
+        ValidationContext<KeyStoreView> ctx = mockValidationContext(
+                null,
+                new StubKeyStoreView("PKCS12", "/path/to/keystore.p12", null)
+        );
+        validate(KeyStoreConfigurationValidatorImpl.INSTANCE, 
mock(KeyStoreConfigurationValidator.class), ctx, null);
+    }
+}
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationModule.java
 
b/modules/network/src/test/java/org/apache/ignite/internal/network/configuration/StubKeyStoreView.java
similarity index 53%
copy from 
modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationModule.java
copy to 
modules/network/src/test/java/org/apache/ignite/internal/network/configuration/StubKeyStoreView.java
index bcee31330e..6afa2128c2 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationModule.java
+++ 
b/modules/network/src/test/java/org/apache/ignite/internal/network/configuration/StubKeyStoreView.java
@@ -15,27 +15,36 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.rest.configuration;
+package org.apache.ignite.internal.network.configuration;
 
-import com.google.auto.service.AutoService;
-import java.util.Collection;
-import java.util.Collections;
-import org.apache.ignite.configuration.RootKey;
-import org.apache.ignite.configuration.annotation.ConfigurationType;
-import org.apache.ignite.internal.configuration.ConfigurationModule;
+/** Stub of {@link KeyStoreView} for tests. */
+public class StubKeyStoreView implements KeyStoreView {
+
+    private String type;
+
+    private String path;
+
+    private String password;
+
+    /** Constructor. */
+    public StubKeyStoreView(String type, String path, String password) {
+        this.type = type;
+        this.path = path;
+        this.password = password;
+    }
+
+    @Override
+    public String type() {
+        return type;
+    }
 
-/**
- * {@link ConfigurationModule} for node-local configuration provided by 
ignite-rest.
- */
-@AutoService(ConfigurationModule.class)
-public class RestConfigurationModule implements ConfigurationModule {
     @Override
-    public ConfigurationType type() {
-        return ConfigurationType.LOCAL;
+    public String path() {
+        return path;
     }
 
     @Override
-    public Collection<RootKey<?, ?>> rootKeys() {
-        return Collections.singleton(RestConfiguration.KEY);
+    public String password() {
+        return password;
     }
 }
diff --git 
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/NodeMetadataDto.java
 
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/NodeMetadataDto.java
index 3d64d162e2..c777febb3b 100644
--- 
a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/NodeMetadataDto.java
+++ 
b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/NodeMetadataDto.java
@@ -29,18 +29,24 @@ import org.apache.ignite.network.NodeMetadata;
 @Schema(name = "NodeMetadata")
 public class NodeMetadataDto {
     private final String restHost;
-    private final int restPort;
+    private final int httpPort;
+    private final int httpsPort;
 
     /**
      * Constructor.
      *
      * @param restHost REST host of a node.
-     * @param restPort REST port of a node.
+     * @param httpPort HTTP port of a node.
+     * @param httpsPort HTTPS port of a node.
      */
     @JsonCreator
-    public NodeMetadataDto(@JsonProperty("restHost") String restHost, 
@JsonProperty("restPort") int restPort) {
+    public NodeMetadataDto(
+            @JsonProperty("restHost") String restHost,
+            @JsonProperty("httpPort") int httpPort,
+            @JsonProperty("httpsPort") int httpsPort) {
         this.restHost = restHost;
-        this.restPort = restPort;
+        this.httpPort = httpPort;
+        this.httpsPort = httpsPort;
     }
 
     /**
@@ -54,12 +60,22 @@ public class NodeMetadataDto {
     }
 
     /**
-     * Returns this node's REST port.
+     * Returns this node's HTTP port.
      *
-     * @return REST port.
+     * @return HTTP port.
      */
-    @JsonGetter("restPort")
-    public int restPort() {
-        return restPort;
+    @JsonGetter("httpPort")
+    public int httpPort() {
+        return httpPort;
+    }
+
+    /**
+     * Returns this node's HTTPS port.
+     *
+     * @return HTTPS port.
+     */
+    @JsonGetter("httpsPort")
+    public int httpsPort() {
+        return httpsPort;
     }
 }
diff --git a/modules/rest/build.gradle b/modules/rest/build.gradle
index 3dc864680b..3e1dea30d4 100644
--- a/modules/rest/build.gradle
+++ b/modules/rest/build.gradle
@@ -30,6 +30,7 @@ dependencies {
     implementation project(':ignite-configuration')
     implementation project(':ignite-rest-api')
     implementation project(':ignite-core')
+    implementation project(':ignite-network')
     implementation libs.jetbrains.annotations
     implementation libs.micronaut.inject
     implementation libs.micronaut.http.server.netty
diff --git a/modules/rest/openapi/openapi.yaml 
b/modules/rest/openapi/openapi.yaml
index 9f37d67525..fcb5edfec0 100644
--- a/modules/rest/openapi/openapi.yaml
+++ b/modules/rest/openapi/openapi.yaml
@@ -516,13 +516,17 @@ components:
           format: int32
     NodeMetadata:
       required:
+      - httpPort
+      - httpsPort
       - restHost
-      - restPort
       type: object
       properties:
         restHost:
           type: string
-        restPort:
+        httpPort:
+          type: integer
+          format: int32
+        httpsPort:
           type: integer
           format: int32
     NodeState:
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
index b0878b207b..d5fc16c084 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
+++ 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
@@ -43,6 +43,7 @@ import 
org.apache.ignite.internal.rest.api.configuration.NodeConfigurationApi;
 import org.apache.ignite.internal.rest.api.metric.NodeMetricApi;
 import org.apache.ignite.internal.rest.api.node.NodeManagementApi;
 import org.apache.ignite.internal.rest.configuration.RestConfiguration;
+import org.apache.ignite.internal.rest.configuration.RestSslView;
 import org.apache.ignite.internal.rest.configuration.RestView;
 import org.apache.ignite.lang.IgniteInternalException;
 import org.jetbrains.annotations.Nullable;
@@ -67,8 +68,9 @@ import org.jetbrains.annotations.Nullable;
         TopologyApi.class
 })
 public class RestComponent implements IgniteComponent {
-    /** Default port. */
-    public static final int DFLT_PORT = 10300;
+
+    /** Unavailable port. */
+    private static final int UNAVAILABLE_PORT = -1;
 
     /** Server host. */
     private static final String LOCALHOST = "localhost";
@@ -85,7 +87,10 @@ public class RestComponent implements IgniteComponent {
     private volatile ApplicationContext context;
 
     /** Server port. */
-    private int port;
+    private int httpPort = UNAVAILABLE_PORT;
+
+    /** Server SSL port. */
+    private int httpsPort = UNAVAILABLE_PORT;
 
     /**
      * Creates a new instance of REST module.
@@ -99,37 +104,77 @@ public class RestComponent implements IgniteComponent {
     @Override
     public void start() {
         RestView restConfigurationView = restConfiguration.value();
+        RestSslView sslConfigurationView = restConfigurationView.ssl();
 
-        int desiredPort = restConfigurationView.port();
+        boolean sslEnabled = sslConfigurationView.enabled();
+        boolean dualProtocol = restConfiguration.dualProtocol().value();
+        int desiredHttpPort = restConfigurationView.port();
         int portRange = restConfigurationView.portRange();
+        int desiredHttpsPort = sslConfigurationView.port();
+        int httpsPortRange = sslConfigurationView.portRange();
+        int httpPortCandidate = desiredHttpPort;
+        int httpsPortCandidate = desiredHttpsPort;
 
-        for (int portCandidate = desiredPort; portCandidate <= desiredPort + 
portRange; portCandidate++) {
-            try {
-                port = portCandidate;
-                context = buildMicronautContext(portCandidate)
-                        .deduceEnvironment(false)
-                        .environments(BARE_METAL)
-                        .start();
-                LOG.info("REST protocol started successfully");
+        while (httpPortCandidate <= desiredHttpPort + portRange
+                && httpsPortCandidate <= desiredHttpsPort + httpsPortRange) {
+            if (startServer(httpPortCandidate, httpsPortCandidate)) {
                 return;
-            } catch (ApplicationStartupException e) {
-                BindException bindException = findBindException(e);
-                if (bindException != null) {
-                    LOG.debug("Got exception during node start, going to try 
again [port={}]", portCandidate);
-                    continue;
-                }
-                throw new RuntimeException(e);
+            }
+
+            LOG.debug("Got exception during node start, going to try again 
[httpPort={}, httpsPort={}]",
+                    httpPortCandidate,
+                    httpsPortCandidate);
+
+            if (sslEnabled && dualProtocol) {
+                httpPortCandidate++;
+                httpsPortCandidate++;
+            } else if (sslEnabled) {
+                httpsPortCandidate++;
+            } else {
+                httpPortCandidate++;
             }
         }
 
-        LOG.debug("Unable to start REST endpoint. All ports are in use 
[ports=[{}, {}]]", desiredPort, (desiredPort + portRange));
+        LOG.debug("Unable to start REST endpoint."
+                        + " Couldn't find available port for HTTP or HTTPS"
+                        + " [HTTP ports=[{}, {}]],"
+                        + " [HTTPS ports=[{}, {}]]",
+                desiredHttpPort, (desiredHttpPort + portRange),
+                desiredHttpsPort, (desiredHttpsPort + httpsPortRange));
 
-        String msg = "Cannot start REST endpoint. " + "All ports in range [" + 
desiredPort + ", " + (desiredPort + portRange)
-                + "] are in use.";
+        String msg = "Cannot start REST endpoint."
+                + " Couldn't find available port for HTTP or HTTPS"
+                + " [HTTP ports=[" + desiredHttpPort + ", " + desiredHttpPort 
+ portRange + "]],"
+                + " [HTTPS ports=[" + desiredHttpsPort + ", " + 
desiredHttpsPort + httpsPortRange + "]]";
 
         throw new RuntimeException(msg);
     }
 
+    /** Starts Micronaut application using the provided ports.
+     *
+     * @param httpPortCandidate HTTP port candidate.
+     * @param httpsPortCandidate HTTPS port candidate.
+     * @return {@code True} if server was started successfully, {@code False} 
if couldn't bind one of the ports.
+     */
+    private boolean startServer(int httpPortCandidate, int httpsPortCandidate) 
{
+        try {
+            httpPort = httpPortCandidate;
+            httpsPort = httpsPortCandidate;
+            context = buildMicronautContext(httpPortCandidate, 
httpsPortCandidate)
+                    .deduceEnvironment(false)
+                    .environments(BARE_METAL)
+                    .start();
+            LOG.info("REST protocol started successfully");
+            return true;
+        } catch (ApplicationStartupException e) {
+            BindException bindException = findBindException(e);
+            if (bindException != null) {
+                return false;
+            }
+            throw new RuntimeException(e);
+        }
+    }
+
     @Nullable
     private BindException findBindException(ApplicationStartupException e) {
         var throwable = e.getCause();
@@ -142,11 +187,11 @@ public class RestComponent implements IgniteComponent {
         return null;
     }
 
-    private Micronaut buildMicronautContext(int portCandidate) {
+    private Micronaut buildMicronautContext(int portCandidate, int 
sslPortCandidate) {
         Micronaut micronaut = Micronaut.build("");
         setFactories(micronaut);
         return micronaut
-                .properties(Map.of("micronaut.server.port", portCandidate))
+                .properties(properties(portCandidate, sslPortCandidate))
                 .banner(false)
                 .mapError(ServerStartupException.class, 
this::mapServerStartupException)
                 .mapError(ApplicationStartupException.class, ex -> -1);
@@ -166,6 +211,28 @@ public class RestComponent implements IgniteComponent {
         }
     }
 
+    private Map<String, Object> properties(int port, int sslPort) {
+        boolean dualProtocol = restConfiguration.dualProtocol().value();
+        boolean sslEnabled = restConfiguration.ssl().enabled().value();
+        String keyStoreType = 
restConfiguration.ssl().keyStore().type().value();
+        String keyStorePath = 
restConfiguration.ssl().keyStore().path().value();
+        String keyStorePassword = 
restConfiguration.ssl().keyStore().password().value();
+
+        if (sslEnabled) {
+            return Map.of(
+                    "micronaut.server.port", port, // Micronaut is not going 
to handle requests on that port, but it's required
+                    "micronaut.server.dual-protocol", dualProtocol,
+                    "micronaut.server.ssl.port", sslPort,
+                    "micronaut.server.ssl.enabled", sslEnabled,
+                    "micronaut.server.ssl.key-store.path", "file:" + 
keyStorePath,
+                    "micronaut.server.ssl.key-store.password", 
keyStorePassword,
+                    "micronaut.server.ssl.key-store.type", keyStoreType
+            );
+        } else {
+            return Map.of("micronaut.server.port", port);
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     public synchronized void stop() throws Exception {
@@ -179,19 +246,37 @@ public class RestComponent implements IgniteComponent {
     /**
      * Returns server port.
      *
+     * @return server port or -1 if HTTP is unavailable.
      * @throws IgniteInternalException if the component has not been started 
yet.
      */
-    public int port() {
-        if (context == null) {
-            throw new IgniteInternalException("RestComponent has not been 
started");
+    public int httpPort() {
+        RestView restView = restConfiguration.value();
+        if (!restView.ssl().enabled() || restView.dualProtocol()) {
+            return httpPort;
+        } else {
+            return UNAVAILABLE_PORT;
         }
+    }
 
-        return port;
+    /**
+     * Returns server SSL port.
+     *
+     * @return server SSL port or -1 if HTTPS is unavailable.
+     * @throws IgniteInternalException if the component has not been started 
yet.
+     */
+    public int httpsPort() {
+        RestView restView = restConfiguration.value();
+        if (restView.ssl().enabled()) {
+            return httpsPort;
+        } else {
+            return UNAVAILABLE_PORT;
+        }
     }
 
     /**
      * Returns server host.
      *
+     * @return host.
      * @throws IgniteInternalException if the component has not been started 
yet.
      */
     public String host() {
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationModule.java
 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationModule.java
index bcee31330e..9558de9f33 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationModule.java
+++ 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationModule.java
@@ -20,9 +20,12 @@ package org.apache.ignite.internal.rest.configuration;
 import com.google.auto.service.AutoService;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Set;
 import org.apache.ignite.configuration.RootKey;
 import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.configuration.validation.Validator;
 import org.apache.ignite.internal.configuration.ConfigurationModule;
+import 
org.apache.ignite.internal.network.configuration.KeyStoreConfigurationValidatorImpl;
 
 /**
  * {@link ConfigurationModule} for node-local configuration provided by 
ignite-rest.
@@ -38,4 +41,9 @@ public class RestConfigurationModule implements 
ConfigurationModule {
     public Collection<RootKey<?, ?>> rootKeys() {
         return Collections.singleton(RestConfiguration.KEY);
     }
+
+    @Override
+    public Set<Validator<?, ?>> validators() {
+        return Set.of(KeyStoreConfigurationValidatorImpl.INSTANCE);
+    }
 }
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
index cabd1c5a6b..6d06d7cf39 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
+++ 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.rest.configuration;
 
+import org.apache.ignite.configuration.annotation.ConfigValue;
 import org.apache.ignite.configuration.annotation.ConfigurationRoot;
 import org.apache.ignite.configuration.annotation.ConfigurationType;
 import org.apache.ignite.configuration.annotation.Value;
@@ -37,4 +38,16 @@ public class RestConfigurationSchema {
     @Range(min = 0)
     @Value(hasDefault = true)
     public final int portRange = 100;
+
+    /** The dual protocol (http/https) configuration. */
+    @Value(hasDefault = true)
+    public final boolean dualProtocol = false;
+
+    /** HTTP to HTTPS redirection. */
+    @Value(hasDefault = true)
+    public final boolean httpToHttpsRedirection = false;
+
+    /** SSL configuration. */
+    @ConfigValue
+    public RestSslConfigurationSchema ssl;
 }
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestSslConfigurationSchema.java
similarity index 61%
copy from 
modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
copy to 
modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestSslConfigurationSchema.java
index cabd1c5a6b..00da64cad5 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestConfigurationSchema.java
+++ 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestSslConfigurationSchema.java
@@ -17,24 +17,33 @@
 
 package org.apache.ignite.internal.rest.configuration;
 
-import org.apache.ignite.configuration.annotation.ConfigurationRoot;
-import org.apache.ignite.configuration.annotation.ConfigurationType;
+import org.apache.ignite.configuration.annotation.Config;
+import org.apache.ignite.configuration.annotation.ConfigValue;
 import org.apache.ignite.configuration.annotation.Value;
 import org.apache.ignite.configuration.validation.Range;
+import 
org.apache.ignite.internal.network.configuration.KeyStoreConfigurationSchema;
+import 
org.apache.ignite.internal.network.configuration.KeyStoreConfigurationValidator;
 
-/**
- * Configuration schema for REST endpoint subtree.
- */
-@SuppressWarnings("PMD.UnusedPrivateField")
-@ConfigurationRoot(rootName = "rest", type = ConfigurationType.LOCAL)
-public class RestConfigurationSchema {
-    /** TCP port. */
+/** REST SSL configuration. */
+@Config
+public class RestSslConfigurationSchema {
+
+    /** Whether SSL is enabled. */
+    @Value(hasDefault = true)
+    public final boolean enabled = false;
+
+    /** SSL port. */
     @Range(min = 1024, max = 0xFFFF)
     @Value(hasDefault = true)
-    public final int port = 10300;
+    public final int port = 10400;
 
-    /** TCP port range. */
+    /** SSL port range. */
     @Range(min = 0)
     @Value(hasDefault = true)
     public final int portRange = 100;
+
+    /** SSL keystore. */
+    @KeyStoreConfigurationValidator
+    @ConfigValue
+    public KeyStoreConfigurationSchema keyStore;
 }
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItPortRangeTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItPortRangeTest.java
new file mode 100644
index 0000000000..3098c447a4
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItPortRangeTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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 org.apache.ignite.internal.rest;
+
+import static 
org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.file.Path;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+import org.apache.ignite.internal.rest.ssl.ItRestSslTest;
+import org.apache.ignite.internal.rest.ssl.RestNode;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/** Tests for the REST port range configuration. */
+@ExtendWith(WorkDirectoryExtension.class)
+public class ItPortRangeTest {
+
+    /** Trust store path. */
+    private static final String trustStorePath = "ssl/truststore.jks";
+
+    /** Trust store password. */
+    private static final String trustStorePassword = "changeIt";
+
+    /** Path to the working directory. */
+    @WorkDirectory
+    private Path workDir;
+
+    /** SSL HTTP client that is expected to be defined in subclasses. */
+    private HttpClient sslClient;
+
+
+    @BeforeEach
+    void beforeEach(TestInfo testInfo)
+            throws CertificateException, KeyStoreException, IOException, 
NoSuchAlgorithmException, KeyManagementException {
+        sslClient = HttpClient.newBuilder()
+                .sslContext(sslContext())
+                .build();
+    }
+
+    private static Stream<Arguments> sslConfigurationProperties() {
+        return Stream.of(
+                Arguments.of(false, false),
+                Arguments.of(false, true),
+                Arguments.of(true, false),
+                Arguments.of(true, true)
+        );
+    }
+
+    @ParameterizedTest
+    @DisplayName("Port range works in all configurations")
+    @MethodSource("sslConfigurationProperties")
+    void portRange(boolean sslEnabled, boolean dualProtocol, TestInfo 
testInfo) throws IOException, InterruptedException {
+        List<RestNode> nodes = IntStream.range(0, 3)
+                .mapToObj(id -> new RestNode(
+                        workDir,
+                        testNodeName(testInfo, id),
+                        3522 + id,
+                        10300,
+                        10400,
+                        true,
+                        true
+                ))
+                .collect(Collectors.toList());
+        try {
+            nodes.forEach(RestNode::start);
+            // When GET /management/v1/configuration/node
+            String httpAddress = sslEnabled ? nodes.get(0).httpsAddress() : 
nodes.get(0).httpAddress();
+            URI uri = URI.create(httpAddress + 
"/management/v1/configuration/node");
+            HttpRequest request = HttpRequest.newBuilder(uri).build();
+
+            // Then response code is 200
+            HttpResponse<String> response = sslClient.send(request, 
BodyHandlers.ofString());
+            assertEquals(200, response.statusCode());
+        } finally {
+            nodes.forEach(RestNode::stop);
+        }
+    }
+
+    private static SSLContext sslContext()
+            throws CertificateException, KeyStoreException, IOException, 
NoSuchAlgorithmException, KeyManagementException {
+        String path = 
ItRestSslTest.class.getClassLoader().getResource(trustStorePath).getPath();
+        TrustManagerFactory trustManagerFactory = 
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+        KeyStore keyStore = KeyStore.getInstance(new File(path), 
trustStorePassword.toCharArray());
+        trustManagerFactory.init(keyStore);
+        SSLContext sslContext = SSLContext.getInstance("TLS");
+        sslContext.init(null, trustManagerFactory.getTrustManagers(), new 
SecureRandom());
+        return sslContext;
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/ItRestSslTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/ItRestSslTest.java
new file mode 100644
index 0000000000..70b5f14722
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/ItRestSslTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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 org.apache.ignite.internal.rest.ssl;
+
+import static 
org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.file.Path;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.util.stream.Stream;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/** Tests for the REST SSL configuration. */
+@ExtendWith(WorkDirectoryExtension.class)
+public class ItRestSslTest {
+
+    /** HTTP port of the test node. */
+    private static final int httpPort = 10300;
+
+    /** HTTPS port of the test node. */
+    private static final int httpsPort = 10400;
+
+    /** Trust store path. */
+    private static final String trustStorePath = "ssl/truststore.jks";
+
+    /** Trust store password. */
+    private static final String trustStorePassword = "changeIt";
+
+    /** Path to the working directory. */
+    @WorkDirectory
+    private static Path workDir;
+
+    /** HTTP client that is expected to be defined in subclasses. */
+    private static HttpClient client;
+
+    /** SSL HTTP client that is expected to be defined in subclasses. */
+    private static HttpClient sslClient;
+
+    private static RestNode httpNode;
+
+    private static RestNode httpsNode;
+
+    private static RestNode dualProtocolNode;
+
+    @BeforeAll
+    static void beforeAll(TestInfo testInfo) throws Exception {
+
+        client = HttpClient.newBuilder()
+                .build();
+
+        sslClient = HttpClient.newBuilder()
+                .sslContext(sslContext())
+                .build();
+
+        httpNode = new RestNode(workDir, testNodeName(testInfo, 3344), 3344, 
10300, 10400, false, false);
+        httpsNode = new RestNode(workDir, testNodeName(testInfo, 3345), 3345, 
10301, 10401, true, false);
+        dualProtocolNode = new RestNode(workDir, testNodeName(testInfo, 3346), 
3346, 10302, 10402, true, true);
+        Stream.of(httpNode, httpsNode, dualProtocolNode)
+                .forEach(RestNode::start);
+    }
+
+    @Test
+    void httpsProtocol() throws IOException, InterruptedException {
+        // When GET /management/v1/configuration/node
+        URI uri = URI.create(httpsNode.httpsAddress() + 
"/management/v1/configuration/node");
+        HttpRequest request = HttpRequest.newBuilder(uri).build();
+
+        // Then response code is 200
+        HttpResponse<String> response = sslClient.send(request, 
BodyHandlers.ofString());
+        assertEquals(200, response.statusCode());
+    }
+
+    @Test
+    void httpProtocol(TestInfo testInfo) throws IOException, 
InterruptedException {
+        // When GET /management/v1/configuration/node
+        URI uri = URI.create(httpNode.httpAddress() + 
"/management/v1/configuration/node");
+        HttpRequest request = HttpRequest.newBuilder(uri).build();
+
+        // Then response code is 200
+        HttpResponse<String> response = client.send(request, 
BodyHandlers.ofString());
+        assertEquals(200, response.statusCode());
+    }
+
+    @Test
+    void dualProtocol() throws IOException, InterruptedException {
+        // When GET /management/v1/configuration/node
+        URI httpUri = URI.create(dualProtocolNode.httpAddress() + 
"/management/v1/configuration/node");
+        HttpRequest httpRequest = HttpRequest.newBuilder(httpUri).build();
+
+        URI httpsUri = URI.create(dualProtocolNode.httpsAddress() + 
"/management/v1/configuration/node");
+        HttpRequest httpsRequest = HttpRequest.newBuilder(httpsUri).build();
+
+        // Then HTTP response code is 200
+        HttpResponse<String> httpResponse = client.send(httpRequest, 
BodyHandlers.ofString());
+        assertEquals(200, httpResponse.statusCode());
+
+        // And HTTPS response code is 200
+        httpResponse = sslClient.send(httpsRequest, BodyHandlers.ofString());
+        assertEquals(200, httpResponse.statusCode());
+    }
+
+    @Test
+    void httpsProtocolNotSslClient() {
+        // When GET /management/v1/configuration/node
+        URI uri = URI.create(httpsNode.httpsAddress() + 
"/management/v1/configuration/node");
+        HttpRequest request = HttpRequest.newBuilder(uri).build();
+
+        // Then IOException
+        assertThrows(IOException.class, () -> client.send(request, 
BodyHandlers.ofString()));
+
+    }
+
+    @Test
+    void httpProtocolNotSslClient() throws IOException, InterruptedException {
+        // When GET /management/v1/configuration/node
+        URI uri = URI.create(httpsNode.httpAddress() + 
"/management/v1/configuration/node");
+        HttpRequest request = HttpRequest.newBuilder(uri).build();
+
+        // Expect IOException
+        assertThrows(IOException.class, () -> client.send(request, 
BodyHandlers.ofString()));
+    }
+
+    @AfterAll
+    static void afterAll() {
+        Stream.of(httpNode, httpsNode, dualProtocolNode)
+                .forEach(RestNode::stop);
+    }
+
+    private static SSLContext sslContext()
+            throws CertificateException, KeyStoreException, IOException, 
NoSuchAlgorithmException, KeyManagementException {
+        String path = 
ItRestSslTest.class.getClassLoader().getResource(trustStorePath).getPath();
+        TrustManagerFactory trustManagerFactory = 
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+        KeyStore keyStore = KeyStore.getInstance(new File(path), 
trustStorePassword.toCharArray());
+        trustManagerFactory.init(keyStore);
+        SSLContext sslContext = SSLContext.getInstance("TLS");
+        sslContext.init(null, trustManagerFactory.getTrustManagers(), new 
SecureRandom());
+        return sslContext;
+    }
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/RestNode.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/RestNode.java
new file mode 100644
index 0000000000..f554526994
--- /dev/null
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/RestNode.java
@@ -0,0 +1,99 @@
+/*
+ * 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 org.apache.ignite.internal.rest.ssl;
+
+import java.nio.file.Path;
+import org.apache.ignite.IgnitionManager;
+
+/** Presentation of Ignite node for tests. */
+public class RestNode {
+
+    /** Key store path. */
+    private static final String keyStorePath = "ssl/keystore.p12";
+
+    /** Key store password. */
+    private static final String keyStorePassword = "changeIt";
+
+    private final Path workDir;
+    private final String name;
+    private final int networkPort;
+    private final int httpPort;
+    private final int httpsPort;
+    private final boolean sslEnabled;
+    private final boolean dualProtocol;
+
+    /** Constructor. */
+    public RestNode(
+            Path workDir,
+            String name,
+            int networkPort,
+            int httpPort,
+            int httpsPort,
+            boolean sslEnabled,
+            boolean dualProtocol
+    ) {
+        this.workDir = workDir;
+        this.name = name;
+        this.networkPort = networkPort;
+        this.httpPort = httpPort;
+        this.httpsPort = httpsPort;
+        this.sslEnabled = sslEnabled;
+        this.dualProtocol = dualProtocol;
+    }
+
+    public RestNode start() {
+        IgnitionManager.start(name, bootstrapCfg(), workDir.resolve(name));
+        return this;
+    }
+
+    public void stop() {
+        IgnitionManager.stop(name);
+    }
+
+    public String httpAddress() {
+        return "http://localhost:"; + httpPort;
+    }
+
+    public String httpsAddress() {
+        return "https://localhost:"; + httpsPort;
+    }
+
+    private String bootstrapCfg() {
+        String keyStoreAbsolutPath = 
ItRestSslTest.class.getClassLoader().getResource(keyStorePath).getPath();
+        return "{\n"
+                + "  network: {\n"
+                + "    port: " + networkPort + ",\n"
+                + "    nodeFinder: {\n"
+                + "      netClusterNodes: [ \"localhost:3344\", 
\"localhost:3345\", \"localhost:3346\" ]\n"
+                + "    }\n"
+                + "  },\n"
+                + "  rest: {\n"
+                + "    port: " + httpPort + ",\n"
+                + "    dualProtocol: " + dualProtocol + ",\n"
+                + "    ssl: {\n"
+                + "      enabled: " + sslEnabled + ",\n"
+                + "      port: " + httpsPort + ",\n"
+                + "      keyStore: {\n"
+                + "        path: " + keyStoreAbsolutPath + ",\n"
+                    + "    password: " + keyStorePassword + "\n"
+                + "      }\n"
+                + "    }\n"
+                + "  }"
+                + "}";
+    }
+}
diff --git a/modules/runner/src/integrationTest/resources/ssl/keystore.p12 
b/modules/runner/src/integrationTest/resources/ssl/keystore.p12
new file mode 100644
index 0000000000..1677d2a561
Binary files /dev/null and 
b/modules/runner/src/integrationTest/resources/ssl/keystore.p12 differ
diff --git a/modules/runner/src/integrationTest/resources/ssl/truststore.jks 
b/modules/runner/src/integrationTest/resources/ssl/truststore.jks
new file mode 100644
index 0000000000..a20d9db42d
Binary files /dev/null and 
b/modules/runner/src/integrationTest/resources/ssl/truststore.jks differ
diff --git 
a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java 
b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
index 008116e5e0..f95df43dd0 100644
--- 
a/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
+++ 
b/modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java
@@ -568,9 +568,9 @@ public class IgniteImpl implements Ignite {
                     cmgMgr
             );
 
-            clusterSvc.updateMetadata(new NodeMetadata(restComponent.host(), 
restComponent.port()));
+            clusterSvc.updateMetadata(new NodeMetadata(restComponent.host(), 
restComponent.httpPort(), restComponent.httpsPort()));
 
-            restAddressReporter.writeReport(restAddress());
+            restAddressReporter.writeReport(restHttpAddress(), 
restHttpsAddress());
 
             LOG.info("Components started, joining the cluster");
 
@@ -755,13 +755,38 @@ public class IgniteImpl implements Ignite {
     }
 
     /**
-     * Returns the local address of REST endpoints.
+     * Returns the local HTTP address of REST endpoints.
      *
+     * @return address or null if HTTP is not enabled.
      * @throws IgniteInternalException if the REST module is not started.
      */
     // TODO: should be encapsulated in local properties, see 
https://issues.apache.org/jira/browse/IGNITE-15131
-    public NetworkAddress restAddress() {
-        return new NetworkAddress(restComponent.host(), restComponent.port());
+    @Nullable
+    public NetworkAddress restHttpAddress() {
+        String host = restComponent.host();
+        int port = restComponent.httpPort();
+        if (port != -1) {
+            return new NetworkAddress(host, port);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns the local HTTPS address of REST endpoints.
+     *
+     * @return address or null if HTTPS is not enabled.
+     * @throws IgniteInternalException if the REST module is not started.
+     */
+    // TODO: should be encapsulated in local properties, see 
https://issues.apache.org/jira/browse/IGNITE-15131
+    public NetworkAddress restHttpsAddress() {
+        String host = restComponent.host();
+        int port = restComponent.httpsPort();
+        if (port != -1) {
+            return new NetworkAddress(host, port);
+        } else {
+            return null;
+        }
     }
 
     /**
diff --git 
a/modules/runner/src/main/java/org/apache/ignite/internal/component/RestAddressReporter.java
 
b/modules/runner/src/main/java/org/apache/ignite/internal/component/RestAddressReporter.java
index 9d389a230d..51e1b2677c 100644
--- 
a/modules/runner/src/main/java/org/apache/ignite/internal/component/RestAddressReporter.java
+++ 
b/modules/runner/src/main/java/org/apache/ignite/internal/component/RestAddressReporter.java
@@ -20,9 +20,13 @@ package org.apache.ignite.internal.component;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.apache.ignite.lang.ErrorGroups.Common;
 import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.network.NetworkAddress;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Can write network address to file that could be used by other systems to 
know on what port Ignite 3 REST server is started.
@@ -39,15 +43,30 @@ public class RestAddressReporter {
     }
 
     /** Write network address to file. */
-    public void writeReport(NetworkAddress networkAddress) {
+    public void writeReport(@Nullable NetworkAddress httpAddress, @Nullable 
NetworkAddress httpsAddress) {
         try {
-            Files.writeString(workDir.resolve(REPORT_FILE_NAME), "http://"; + 
networkAddress.host() + ":" + networkAddress.port());
+            Files.writeString(workDir.resolve(REPORT_FILE_NAME), 
report(httpAddress, httpsAddress));
         } catch (IOException e) {
             String message = "Unexpected error when trying to write REST 
server network address to file";
             throw new IgniteException(Common.UNEXPECTED_ERR, message, e);
         }
     }
 
+    private String report(@Nullable NetworkAddress httpAddress, @Nullable 
NetworkAddress httpsAddress) {
+        return Stream.of(report("http", httpAddress), report("https", 
httpsAddress))
+                .filter(Objects::nonNull)
+                .collect(Collectors.joining(", "));
+    }
+
+    @Nullable
+    private String report(String protocol, @Nullable NetworkAddress 
httpAddress) {
+        if (httpAddress == null) {
+            return null;
+        } else {
+            return protocol + "://" + httpAddress.host() + ":" + 
httpAddress.port();
+        }
+    }
+
     /** Remove report file. The method is expected to be called on node stop. 
*/
     public void removeReport() {
         try {
diff --git 
a/modules/runner/src/test/java/org/apache/ignite/internal/component/RestAddressReporterTest.java
 
b/modules/runner/src/test/java/org/apache/ignite/internal/component/RestAddressReporterTest.java
index 3463100dde..174cd12a16 100644
--- 
a/modules/runner/src/test/java/org/apache/ignite/internal/component/RestAddressReporterTest.java
+++ 
b/modules/runner/src/test/java/org/apache/ignite/internal/component/RestAddressReporterTest.java
@@ -39,25 +39,53 @@ class RestAddressReporterTest {
     private static final String REST_ADDRESS_FILENAME = "rest-address";
 
     @Test
-    @DisplayName("REST server network address is reported to file")
-    void networkAddressReported(@TempDir Path tmpDir) throws IOException {
+    @DisplayName("REST server network addresses is reported to file")
+    void httpAndHttpsAddressesReported(@TempDir Path tmpDir) throws 
IOException {
         // Given
         RestAddressReporter reporter = new RestAddressReporter(tmpDir);
 
         // When
-        reporter.writeReport(new NetworkAddress("localhost", 9999));
+        reporter.writeReport(new NetworkAddress("localhost", 9999), new 
NetworkAddress("localhost", 8443));
+
+        // Then there is a report
+        String restAddress = 
Files.readString(tmpDir.resolve(REST_ADDRESS_FILENAME));
+        assertThat(restAddress, equalTo("http://localhost:9999, 
https://localhost:8443";));
+    }
+
+    @Test
+    @DisplayName("REST server HTTP address is reported to file")
+    void httpAddressReported(@TempDir Path tmpDir) throws IOException {
+        // Given
+        RestAddressReporter reporter = new RestAddressReporter(tmpDir);
+
+        // When
+        reporter.writeReport(new NetworkAddress("localhost", 9999), null);
 
         // Then there is a report
         String restAddress = 
Files.readString(tmpDir.resolve(REST_ADDRESS_FILENAME));
         assertThat(restAddress, equalTo("http://localhost:9999";));
     }
 
+    @Test
+    @DisplayName("REST server HTTPS address is reported to file")
+    void httpsAddressReported(@TempDir Path tmpDir) throws IOException {
+        // Given
+        RestAddressReporter reporter = new RestAddressReporter(tmpDir);
+
+        // When
+        reporter.writeReport(null, new NetworkAddress("localhost", 8443));
+
+        // Then there is a report
+        String restAddress = 
Files.readString(tmpDir.resolve(REST_ADDRESS_FILENAME));
+        assertThat(restAddress, equalTo("https://localhost:8443";));
+    }
+
     @Test
     @DisplayName("File with network address is removed")
     void reportDeleted(@TempDir Path tmpDir) throws IOException {
         // Given reported address
         RestAddressReporter reporter = new RestAddressReporter(tmpDir);
-        reporter.writeReport(new NetworkAddress("localhost", 9999));
+        reporter.writeReport(new NetworkAddress("localhost", 9999), new 
NetworkAddress("localhost", 8443));
         // And file exists
         assertThat(Files.exists(tmpDir.resolve(REST_ADDRESS_FILENAME)), 
is(true));
 
@@ -94,10 +122,11 @@ class RestAddressReporterTest {
         );
 
         // When try to write it again but with another port
-        new RestAddressReporter(tmpDir).writeReport(new 
NetworkAddress("localhost", 4444));
+        RestAddressReporter reporter = new RestAddressReporter(tmpDir);
+        reporter.writeReport(new NetworkAddress("localhost", 4444), new 
NetworkAddress("localhost", 8443));
 
         // Then file rewritten
         String restAddress = 
Files.readString(tmpDir.resolve(REST_ADDRESS_FILENAME));
-        assertThat(restAddress, equalTo("http://localhost:4444";));
+        assertThat(restAddress, equalTo("http://localhost:4444, 
https://localhost:8443";));
     }
 }
diff --git a/packaging/zip/ignite3db b/packaging/zip/ignite3db
index 898af35d36..12b004aa7f 100755
--- a/packaging/zip/ignite3db
+++ b/packaging/zip/ignite3db
@@ -41,7 +41,7 @@ start() {
   while [ ! -f "$rest_address_file" ]; do sleep 0.5; done
     rest_address=$(cat "$rest_address_file")
 
-  echo "Node named ${NODE_NAME} started successfully. REST address is 
$rest_address"
+  echo "Node named ${NODE_NAME} started successfully. REST addresses are 
[$rest_address]"
 }
 
 stop() {

Reply via email to