You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by al...@apache.org on 2020/12/08 06:18:51 UTC

[ignite] branch master updated: IGNITE-13320 Cache encryption key rotation CLI management - Fixes #8242.

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

alexpl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new 8678857  IGNITE-13320 Cache encryption key rotation CLI management - Fixes #8242.
8678857 is described below

commit 8678857ebf033b696ab7196c87cd481b184bea9a
Author: Pavel Pereslegin <xx...@gmail.com>
AuthorDate: Mon Dec 7 18:21:15 2020 +0300

    IGNITE-13320 Cache encryption key rotation CLI management - Fixes #8242.
    
    Signed-off-by: Aleksey Plekhanov <pl...@gmail.com>
---
 .../ignite/internal/commandline/CommandList.java   |   4 +-
 .../encryption/CacheGroupEncryptionCommand.java    | 264 +++++++++++++++++++++
 .../encryption/ChangeCacheGroupKeyCommand.java     |  95 ++++++++
 .../encryption/ChangeMasterKeyCommand.java         |  88 +++++++
 .../commandline/encryption/EncryptionCommand.java  | 130 ----------
 .../commandline/encryption/EncryptionCommands.java |  70 ++++++
 .../encryption/EncryptionSubcommand.java           |  59 -----
 .../encryption/EncryptionSubcommands.java          |  99 ++++++++
 .../encryption/GetMasterKeyNameCommand.java        |  73 ++++++
 .../encryption/ReencryptionRateCommand.java        | 137 +++++++++++
 .../util/GridCommandHandlerAbstractTest.java       |  30 ++-
 .../apache/ignite/util/GridCommandHandlerTest.java | 205 +++++++++++++++-
 .../configuration/EncryptionConfiguration.java     |   2 +-
 .../managers/encryption/CacheGroupPageScanner.java |  86 ++++++-
 .../managers/encryption/GridEncryptionManager.java |  48 ++++
 .../processors/cache/CacheGroupMetricsImpl.java    |  46 +---
 .../ignite/internal/util/BasicRateLimiter.java     |  20 +-
 .../encryption/VisorCacheGroupEncryptionTask.java  | 137 +++++++++++
 .../VisorCacheGroupEncryptionTaskArg.java          |  68 ++++++
 .../VisorCacheGroupEncryptionTaskResult.java       |  90 +++++++
 .../encryption/VisorChangeCacheGroupKeyTask.java   |  61 +++++
 .../encryption/VisorEncryptionKeyIdsTask.java      |  83 +++++++
 .../encryption/VisorReencryptionRateTask.java      | 133 +++++++++++
 .../encryption/VisorReencryptionRateTaskArg.java   |  74 ++++++
 .../encryption/VisorReencryptionResumeTask.java    |  60 +++++
 .../encryption/VisorReencryptionStatusTask.java    |  88 +++++++
 .../encryption/VisorReencryptionSuspendTask.java   |  83 +++++++
 .../main/resources/META-INF/classnames.properties  |  19 ++
 .../encryption/CacheGroupReencryptionTest.java     |  12 +-
 .../ignite/internal/util/BasicRateLimiterTest.java |  15 ++
 ...ridCommandHandlerClusterByClassTest_help.output |  21 ++
 ...andHandlerClusterByClassWithSSLTest_help.output |  21 ++
 32 files changed, 2163 insertions(+), 258 deletions(-)

diff --git a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/CommandList.java b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/CommandList.java
index e9f8cd3..7c45a36 100644
--- a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/CommandList.java
+++ b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/CommandList.java
@@ -19,7 +19,7 @@ package org.apache.ignite.internal.commandline;
 
 import org.apache.ignite.internal.commandline.cache.CacheCommands;
 import org.apache.ignite.internal.commandline.diagnostic.DiagnosticCommand;
-import org.apache.ignite.internal.commandline.encryption.EncryptionCommand;
+import org.apache.ignite.internal.commandline.encryption.EncryptionCommands;
 import org.apache.ignite.internal.commandline.meta.MetadataCommand;
 import org.apache.ignite.internal.commandline.metric.MetricCommand;
 import org.apache.ignite.internal.commandline.property.PropertyCommand;
@@ -59,7 +59,7 @@ public enum CommandList {
     DIAGNOSTIC("--diagnostic", new DiagnosticCommand()),
 
     /** Encryption features command. */
-    ENCRYPTION("--encryption", new EncryptionCommand()),
+    ENCRYPTION("--encryption", new EncryptionCommands()),
 
     /** Kill command. */
     KILL("--kill", new KillCommand()),
diff --git a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/CacheGroupEncryptionCommand.java b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/CacheGroupEncryptionCommand.java
new file mode 100644
index 0000000..d4c09b8
--- /dev/null
+++ b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/CacheGroupEncryptionCommand.java
@@ -0,0 +1,264 @@
+/*
+ * 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.commandline.encryption;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.logging.Logger;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.internal.client.GridClient;
+import org.apache.ignite.internal.client.GridClientConfiguration;
+import org.apache.ignite.internal.commandline.AbstractCommand;
+import org.apache.ignite.internal.commandline.Command;
+import org.apache.ignite.internal.commandline.CommandArgIterator;
+import org.apache.ignite.internal.commandline.CommandList;
+import org.apache.ignite.internal.commandline.CommandLogger;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.visor.encryption.VisorCacheGroupEncryptionTaskArg;
+import org.apache.ignite.internal.visor.encryption.VisorCacheGroupEncryptionTaskResult;
+import org.apache.ignite.internal.visor.encryption.VisorEncryptionKeyIdsTask;
+import org.apache.ignite.internal.visor.encryption.VisorReencryptionResumeTask;
+import org.apache.ignite.internal.visor.encryption.VisorReencryptionStatusTask;
+import org.apache.ignite.internal.visor.encryption.VisorReencryptionSuspendTask;
+
+import static org.apache.ignite.internal.commandline.CommandList.ENCRYPTION;
+import static org.apache.ignite.internal.commandline.CommandLogger.DOUBLE_INDENT;
+import static org.apache.ignite.internal.commandline.CommandLogger.INDENT;
+import static org.apache.ignite.internal.commandline.TaskExecutor.BROADCAST_UUID;
+import static org.apache.ignite.internal.commandline.TaskExecutor.executeTaskByNameOnNode;
+import static org.apache.ignite.internal.commandline.encryption.EncryptionSubcommands.CACHE_GROUP_KEY_IDS;
+
+/**
+ * Base cache group encryption multinode subcommand.
+ *
+ * @param <T> Command result type.
+ */
+public abstract class CacheGroupEncryptionCommand<T> extends AbstractCommand<VisorCacheGroupEncryptionTaskArg> {
+    /** Cache group reencryption task argument. */
+    private VisorCacheGroupEncryptionTaskArg taskArg;
+
+    /** {@inheritDoc} */
+    @Override public VisorCacheGroupEncryptionTaskArg arg() {
+        return taskArg;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void parseArguments(CommandArgIterator argIter) {
+        String grpName = argIter.nextArg("Сache group name is expected.");
+
+        if (argIter.hasNextSubArg())
+            throw new IllegalArgumentException("Unexpected command argument: " + argIter.peekNextArg());
+
+        taskArg = new VisorCacheGroupEncryptionTaskArg(grpName);
+    }
+
+    /** {@inheritDoc} */
+    @Override public Object execute(GridClientConfiguration clientCfg, Logger log) throws Exception {
+        try (GridClient client = Command.startClient(clientCfg)) {
+            VisorCacheGroupEncryptionTaskResult<T> res = executeTaskByNameOnNode(
+                client,
+                visorTaskName(),
+                taskArg,
+                BROADCAST_UUID,
+                clientCfg
+            );
+
+            printResults(res, taskArg.groupName(), log);
+
+            return res;
+        }
+        catch (Throwable e) {
+            log.severe("Failed to perform operation.");
+            log.severe(CommandLogger.errorMessage(e));
+
+            throw e;
+        }
+    }
+
+    /**
+     * @param res Response.
+     * @param grpName Cache group name.
+     * @param log Logger.
+     */
+    protected void printResults(VisorCacheGroupEncryptionTaskResult<T> res, String grpName, Logger log) {
+        Map<UUID, IgniteException> exceptions = res.exceptions();
+
+        for (Map.Entry<UUID, IgniteException> entry : exceptions.entrySet()) {
+            log.info(INDENT + "Node " + entry.getKey() + ":");
+
+            log.info(String.format("%sfailed to execute command for the cache group \"%s\": %s.",
+                DOUBLE_INDENT, grpName, entry.getValue().getMessage()));
+        }
+
+        Map<UUID, T> results = res.results();
+
+        for (Map.Entry<UUID, T> entry : results.entrySet()) {
+            log.info(INDENT + "Node " + entry.getKey() + ":");
+
+            printNodeResult(entry.getValue(), grpName, log);
+        }
+    }
+
+    /**
+     * @param res Response.
+     * @param grpName Cache group name.
+     * @param log Logger.
+     */
+    protected abstract void printNodeResult(T res, String grpName, Logger log);
+
+    /**
+     * @return Visor task name.
+     */
+    protected abstract String visorTaskName();
+
+    /** Subcommand to Display re-encryption status of the cache group. */
+    protected static class ReencryptionStatus extends CacheGroupEncryptionCommand<Long> {
+        /** {@inheritDoc} */
+        @Override protected void printNodeResult(Long bytesLeft, String grpName, Logger log) {
+            if (bytesLeft == -1)
+                log.info(DOUBLE_INDENT + "re-encryption completed or not required");
+            else if (bytesLeft == 0)
+                log.info(DOUBLE_INDENT + "re-encryption will be completed after the next checkpoint");
+            else
+                log.info(String.format("%s%d KB of data left for re-encryption", DOUBLE_INDENT, bytesLeft / 1024));
+        }
+
+        /** {@inheritDoc} */
+        @Override protected String visorTaskName() {
+            return VisorReencryptionStatusTask.class.getName();
+        }
+
+        /** {@inheritDoc} */
+        @Override public String name() {
+            return EncryptionSubcommands.REENCRYPTION_STATUS.text().toUpperCase();
+        }
+
+        /** {@inheritDoc} */
+        @Override public void printUsage(Logger log) {
+            Command.usage(log, "Display re-encryption status of the cache group:", CommandList.ENCRYPTION,
+                EncryptionSubcommands.REENCRYPTION_STATUS.toString(), "cacheGroupName");
+        }
+    }
+
+    /** Subcommand to view current encryption key IDs of the cache group. */
+    protected static class CacheKeyIds extends CacheGroupEncryptionCommand<List<Integer>> {
+        /** {@inheritDoc} */
+        @Override protected void printResults(
+            VisorCacheGroupEncryptionTaskResult<List<Integer>> res,
+            String grpName,
+            Logger log
+        ) {
+            log.info("Encryption key identifiers for cache: " + grpName);
+
+            super.printResults(res, grpName, log);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void printNodeResult(List<Integer> keyIds, String grpName, Logger log) {
+            if (F.isEmpty(keyIds)) {
+                log.info(DOUBLE_INDENT + "---");
+
+                return;
+            }
+
+            for (int i = 0; i < keyIds.size(); i++)
+                log.info(DOUBLE_INDENT + keyIds.get(i) + (i == 0 ? " (active)" : ""));
+        }
+
+        /** {@inheritDoc} */
+        @Override protected String visorTaskName() {
+            return VisorEncryptionKeyIdsTask.class.getName();
+        }
+
+        /** {@inheritDoc} */
+        @Override public String name() {
+            return CACHE_GROUP_KEY_IDS.text().toUpperCase();
+        }
+
+        /** {@inheritDoc} */
+        @Override public void printUsage(Logger log) {
+            Command.usage(log, "View encryption key identifiers of the cache group:", ENCRYPTION,
+                CACHE_GROUP_KEY_IDS.toString(), "cacheGroupName");
+        }
+    }
+
+    /** Subcommand to suspend re-encryption of the cache group. */
+    protected static class SuspendReencryption extends CacheGroupEncryptionCommand<Boolean> {
+        /** {@inheritDoc} */
+        @Override protected String visorTaskName() {
+            return VisorReencryptionSuspendTask.class.getName();
+        }
+
+        /** {@inheritDoc} */
+        @Override public String name() {
+            return EncryptionSubcommands.REENCRYPTION_SUSPEND.text().toUpperCase();
+        }
+
+        /** {@inheritDoc} */
+        @Override public void printUsage(Logger log) {
+            Command.usage(log, "Suspend re-encryption of the cache group:", CommandList.ENCRYPTION,
+                EncryptionSubcommands.REENCRYPTION_SUSPEND.toString(), "cacheGroupName");
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void printNodeResult(Boolean success, String grpName, Logger log) {
+            log.info(String.format("%sre-encryption of the cache group \"%s\" has %sbeen suspended.",
+                DOUBLE_INDENT, grpName, (success ? "" : "already ")));
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void printResults(
+            VisorCacheGroupEncryptionTaskResult<Boolean> res,
+            String grpName,
+            Logger log
+        ) {
+            super.printResults(res, grpName, log);
+
+            log.info("");
+            log.info("Note: the re-encryption suspend status is not persisted, re-encryption will be started " +
+                "automatically after the node is restarted.");
+            log.info("");
+        }
+    }
+
+    /** Subcommand to resume re-encryption of the cache group. */
+    protected static class ResumeReencryption extends CacheGroupEncryptionCommand<Boolean> {
+        /** {@inheritDoc} */
+        @Override protected String visorTaskName() {
+            return VisorReencryptionResumeTask.class.getName();
+        }
+
+        /** {@inheritDoc} */
+        @Override public String name() {
+            return EncryptionSubcommands.REENCRYPTION_RESUME.text().toUpperCase();
+        }
+
+        /** {@inheritDoc} */
+        @Override public void printUsage(Logger log) {
+            Command.usage(log, "Resume re-encryption of the cache group:", CommandList.ENCRYPTION,
+                EncryptionSubcommands.REENCRYPTION_RESUME.toString(), "cacheGroupName");
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void printNodeResult(Boolean success, String grpName, Logger log) {
+            log.info(String.format("%sre-encryption of the cache group \"%s\" has %sbeen resumed.",
+                DOUBLE_INDENT, grpName, (success ? "" : "already ")));
+        }
+    }
+}
diff --git a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/ChangeCacheGroupKeyCommand.java b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/ChangeCacheGroupKeyCommand.java
new file mode 100644
index 0000000..8518e5d
--- /dev/null
+++ b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/ChangeCacheGroupKeyCommand.java
@@ -0,0 +1,95 @@
+/*
+ * 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.commandline.encryption;
+
+import java.util.logging.Logger;
+import org.apache.ignite.internal.client.GridClient;
+import org.apache.ignite.internal.client.GridClientConfiguration;
+import org.apache.ignite.internal.commandline.AbstractCommand;
+import org.apache.ignite.internal.commandline.Command;
+import org.apache.ignite.internal.commandline.CommandArgIterator;
+import org.apache.ignite.internal.commandline.CommandLogger;
+import org.apache.ignite.internal.visor.encryption.VisorCacheGroupEncryptionTaskArg;
+import org.apache.ignite.internal.visor.encryption.VisorChangeCacheGroupKeyTask;
+
+import static org.apache.ignite.internal.commandline.CommandList.ENCRYPTION;
+import static org.apache.ignite.internal.commandline.TaskExecutor.executeTaskByNameOnNode;
+import static org.apache.ignite.internal.commandline.encryption.EncryptionSubcommands.CHANGE_CACHE_GROUP_KEY;
+
+/**
+ * Change cache group key encryption subcommand.
+ */
+public class ChangeCacheGroupKeyCommand extends AbstractCommand<VisorCacheGroupEncryptionTaskArg> {
+    /** Change cache group key task argument. */
+    private VisorCacheGroupEncryptionTaskArg taskArg;
+
+    /** {@inheritDoc} */
+    @Override public Object execute(GridClientConfiguration clientCfg, Logger log) throws Exception {
+        try (GridClient client = Command.startClient(clientCfg)) {
+            executeTaskByNameOnNode(
+                client,
+                VisorChangeCacheGroupKeyTask.class.getName(),
+                taskArg,
+                null,
+                clientCfg
+            );
+
+            log.info("The encryption key has been changed for the cache group \"" + taskArg.groupName() + "\".");
+
+            return null;
+        }
+        catch (Throwable e) {
+            log.severe("Failed to perform operation.");
+            log.severe(CommandLogger.errorMessage(e));
+
+            throw e;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public String confirmationPrompt() {
+        return "Warning: the command will change the encryption key of the cache group. Joining a node during " +
+            "the key change process is prohibited and will be rejected.";
+    }
+
+    /** {@inheritDoc} */
+    @Override public VisorCacheGroupEncryptionTaskArg arg() {
+        return taskArg;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void parseArguments(CommandArgIterator argIter) {
+        String argCacheGrpName = argIter.nextArg("Сache group name is expected.");
+
+        taskArg = new VisorCacheGroupEncryptionTaskArg(argCacheGrpName);
+
+        if (argIter.hasNextSubArg())
+            throw new IllegalArgumentException("Unexpected command argument: " + argIter.peekNextArg());
+    }
+
+    /** {@inheritDoc} */
+    @Override public void printUsage(Logger log) {
+        Command.usage(log, "Change the encryption key of the cache group:", ENCRYPTION,
+            CHANGE_CACHE_GROUP_KEY.toString(), "cacheGroupName");
+    }
+
+    /** {@inheritDoc} */
+    @Override public String name() {
+        return CHANGE_CACHE_GROUP_KEY.text().toUpperCase();
+    }
+}
diff --git a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/ChangeMasterKeyCommand.java b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/ChangeMasterKeyCommand.java
new file mode 100644
index 0000000..a48dc4b
--- /dev/null
+++ b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/ChangeMasterKeyCommand.java
@@ -0,0 +1,88 @@
+/*
+ * 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.commandline.encryption;
+
+import java.util.logging.Logger;
+import org.apache.ignite.internal.client.GridClient;
+import org.apache.ignite.internal.client.GridClientConfiguration;
+import org.apache.ignite.internal.commandline.AbstractCommand;
+import org.apache.ignite.internal.commandline.Command;
+import org.apache.ignite.internal.commandline.CommandArgIterator;
+import org.apache.ignite.internal.commandline.CommandLogger;
+import org.apache.ignite.internal.visor.encryption.VisorChangeMasterKeyTask;
+
+import static org.apache.ignite.internal.commandline.CommandList.ENCRYPTION;
+import static org.apache.ignite.internal.commandline.TaskExecutor.executeTaskByNameOnNode;
+import static org.apache.ignite.internal.commandline.encryption.EncryptionSubcommands.CHANGE_MASTER_KEY;
+
+/**
+ * Change master key encryption subcommand.
+ */
+public class ChangeMasterKeyCommand extends AbstractCommand<String> {
+    /** New master key name. */
+    private String argMasterKeyName;
+
+    /** {@inheritDoc} */
+    @Override public Object execute(GridClientConfiguration clientCfg, Logger log) throws Exception {
+        try (GridClient client = Command.startClient(clientCfg)) {
+            String resMsg = executeTaskByNameOnNode(
+                client,
+                VisorChangeMasterKeyTask.class.getName(),
+                argMasterKeyName,
+                null,
+                clientCfg
+            );
+
+            log.info(resMsg);
+
+            return resMsg;
+        }
+        catch (Throwable e) {
+            log.severe("Failed to perform operation.");
+            log.severe(CommandLogger.errorMessage(e));
+
+            throw e;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public String confirmationPrompt() {
+        return "Warning: the command will change the master key. Cache start and node join during the key change " +
+            "process is prohibited and will be rejected.";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String arg() {
+        return argMasterKeyName;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void parseArguments(CommandArgIterator argIter) {
+        argMasterKeyName = argIter.nextArg("Expected master key name.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void printUsage(Logger log) {
+        Command.usage(log, "Change the master key:", ENCRYPTION, CHANGE_MASTER_KEY.toString(), "newMasterKeyName");
+    }
+
+    /** {@inheritDoc} */
+    @Override public String name() {
+        return CHANGE_MASTER_KEY.text().toUpperCase();
+    }
+}
diff --git a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/EncryptionCommand.java b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/EncryptionCommand.java
deleted file mode 100644
index 5cbd723..0000000
--- a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/EncryptionCommand.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * 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.commandline.encryption;
-
-import java.util.logging.Logger;
-import org.apache.ignite.internal.client.GridClient;
-import org.apache.ignite.internal.client.GridClientConfiguration;
-import org.apache.ignite.internal.commandline.AbstractCommand;
-import org.apache.ignite.internal.commandline.Command;
-import org.apache.ignite.internal.commandline.CommandArgIterator;
-import org.apache.ignite.internal.commandline.CommandLogger;
-import org.apache.ignite.internal.visor.encryption.VisorChangeMasterKeyTask;
-import org.apache.ignite.internal.visor.encryption.VisorGetMasterKeyNameTask;
-
-import static org.apache.ignite.internal.commandline.CommandList.ENCRYPTION;
-import static org.apache.ignite.internal.commandline.TaskExecutor.executeTaskByNameOnNode;
-import static org.apache.ignite.internal.commandline.encryption.EncryptionSubcommand.CHANGE_MASTER_KEY;
-import static org.apache.ignite.internal.commandline.encryption.EncryptionSubcommand.GET_MASTER_KEY_NAME;
-import static org.apache.ignite.internal.commandline.encryption.EncryptionSubcommand.of;
-
-/**
- * Commands assosiated with encryption features.
- *
- * @see EncryptionSubcommand
- */
-public class EncryptionCommand extends AbstractCommand<Object> {
-    /** Subcommand. */
-    EncryptionSubcommand cmd;
-
-    /** The task name. */
-    String taskName;
-
-    /** The task arguments. */
-    Object taskArgs;
-
-    /** {@inheritDoc} */
-    @Override public Object execute(GridClientConfiguration clientCfg, Logger logger) throws Exception {
-        try (GridClient client = Command.startClient(clientCfg)) {
-            String res = executeTaskByNameOnNode(
-                client,
-                taskName,
-                taskArgs,
-                null,
-                clientCfg
-            );
-
-            logger.info(res);
-
-            return res;
-        }
-        catch (Throwable e) {
-            logger.severe("Failed to perform operation.");
-            logger.severe(CommandLogger.errorMessage(e));
-
-            throw e;
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Override public String confirmationPrompt() {
-        if (CHANGE_MASTER_KEY == cmd) {
-            return "Warning: the command will change the master key. Cache start and node join during the key change " +
-                "process is prohibited and will be rejected.";
-        }
-
-        return null;
-    }
-
-    /** {@inheritDoc} */
-    @Override public void parseArguments(CommandArgIterator argIter) {
-        EncryptionSubcommand cmd = of(argIter.nextArg("Expected encryption action."));
-
-        if (cmd == null)
-            throw new IllegalArgumentException("Expected correct encryption action.");
-
-        switch (cmd) {
-            case GET_MASTER_KEY_NAME:
-                taskName = VisorGetMasterKeyNameTask.class.getName();
-
-                taskArgs = null;
-
-                break;
-
-            case CHANGE_MASTER_KEY:
-                String masterKeyName = argIter.nextArg("Expected master key name.");
-
-                taskName = VisorChangeMasterKeyTask.class.getName();
-
-                taskArgs = masterKeyName;
-
-                break;
-
-            default:
-                throw new IllegalArgumentException("Unknown encryption subcommand: " + cmd);
-        }
-
-        this.cmd = cmd;
-    }
-
-    /** {@inheritDoc} */
-    @Override public Object arg() {
-        return taskArgs;
-    }
-
-    /** {@inheritDoc} */
-    @Override public void printUsage(Logger logger) {
-        Command.usage(logger, "Print the current master key name:", ENCRYPTION, GET_MASTER_KEY_NAME.toString());
-        Command.usage(logger, "Change the master key:", ENCRYPTION, CHANGE_MASTER_KEY.toString(), "newMasterKeyName");
-    }
-
-    /** {@inheritDoc} */
-    @Override public String name() {
-        return ENCRYPTION.toCommandName();
-    }
-}
diff --git a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/EncryptionCommands.java b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/EncryptionCommands.java
new file mode 100644
index 0000000..fbae770
--- /dev/null
+++ b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/EncryptionCommands.java
@@ -0,0 +1,70 @@
+/*
+ * 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.commandline.encryption;
+
+import java.util.logging.Logger;
+import org.apache.ignite.internal.client.GridClientConfiguration;
+import org.apache.ignite.internal.commandline.AbstractCommand;
+import org.apache.ignite.internal.commandline.CommandArgIterator;
+import org.apache.ignite.internal.commandline.CommandList;
+
+/**
+ * Commands related to encryption functions.
+ *
+ * @see EncryptionSubcommands
+ */
+public class EncryptionCommands extends AbstractCommand<EncryptionSubcommands> {
+    /** Subcommand. */
+    private EncryptionSubcommands cmd;
+
+    /** {@inheritDoc} */
+    @Override public Object execute(GridClientConfiguration clientCfg, Logger logger) throws Exception {
+        return cmd.subcommand().execute(clientCfg, logger);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void parseArguments(CommandArgIterator argIter) {
+        EncryptionSubcommands cmd = EncryptionSubcommands.of(argIter.nextArg("Expected encryption action."));
+
+        if (cmd == null)
+            throw new IllegalArgumentException("Expected correct encryption action.");
+
+        cmd.subcommand().parseArguments(argIter);
+
+        if (argIter.hasNextSubArg())
+            throw new IllegalArgumentException("Unexpected argument of --encryption subcommand: " + argIter.peekNextArg());
+
+        this.cmd = cmd;
+    }
+
+    /** {@inheritDoc} */
+    @Override public EncryptionSubcommands arg() {
+        return cmd;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void printUsage(Logger logger) {
+        for (EncryptionSubcommands cmd : EncryptionSubcommands.values())
+            cmd.subcommand().printUsage(logger);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String name() {
+        return CommandList.ENCRYPTION.toCommandName();
+    }
+}
diff --git a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/EncryptionSubcommand.java b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/EncryptionSubcommand.java
deleted file mode 100644
index 3c47c02..0000000
--- a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/EncryptionSubcommand.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.commandline.encryption;
-
-import org.jetbrains.annotations.Nullable;
-
-/**
- * Set of encryption subcommands.
- *
- * @see EncryptionCommand
- */
-public enum EncryptionSubcommand {
-    /** Subcommand to get the current master key name. */
-    GET_MASTER_KEY_NAME("get_master_key_name"),
-
-    /** Subcommand to change the master key. */
-    CHANGE_MASTER_KEY("change_master_key");
-
-    /** Subcommand name. */
-    private final String name;
-
-    /** @param name Encryption subcommand name. */
-    EncryptionSubcommand(String name) {
-        this.name = name;
-    }
-
-    /**
-     * @param text Command text (case insensitive).
-     * @return Command for the text. {@code Null} if there is no such command.
-     */
-     @Nullable public static EncryptionSubcommand of(String text) {
-        for (EncryptionSubcommand cmd : EncryptionSubcommand.values()) {
-            if (cmd.name.equalsIgnoreCase(text))
-                return cmd;
-        }
-
-        return null;
-    }
-
-    /** {@inheritDoc} */
-    @Override public String toString() {
-        return name;
-    }
-}
diff --git a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/EncryptionSubcommands.java b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/EncryptionSubcommands.java
new file mode 100644
index 0000000..c8d0941
--- /dev/null
+++ b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/EncryptionSubcommands.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.commandline.encryption;
+
+import org.apache.ignite.internal.commandline.Command;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Set of encryption subcommands.
+ *
+ * @see EncryptionCommands
+ */
+public enum EncryptionSubcommands {
+    /** Subcommand to get the current master key name. */
+    GET_MASTER_KEY_NAME("get_master_key_name", new GetMasterKeyNameCommand()),
+
+    /** Subcommand to change the master key. */
+    CHANGE_MASTER_KEY("change_master_key", new ChangeMasterKeyCommand()),
+
+    /** Subcommand to change the current encryption key for specified cache group. */
+    CHANGE_CACHE_GROUP_KEY("change_cache_key", new ChangeCacheGroupKeyCommand()),
+
+    /** Subcommand to view current encryption key IDs of the cache group. */
+    CACHE_GROUP_KEY_IDS("cache_key_ids", new CacheGroupEncryptionCommand.CacheKeyIds()),
+
+    /** Subcommand to display re-encryption status of the cache group. */
+    REENCRYPTION_STATUS("reencryption_status", new CacheGroupEncryptionCommand.ReencryptionStatus()),
+
+    /** Subcommand to suspend re-encryption of the cache group. */
+    REENCRYPTION_SUSPEND("suspend_reencryption", new CacheGroupEncryptionCommand.SuspendReencryption()),
+
+    /** Subcommand to resume re-encryption of the cache group. */
+    REENCRYPTION_RESUME("resume_reencryption", new CacheGroupEncryptionCommand.ResumeReencryption()),
+
+    /** Subcommand to view/change cache group re-encryption rate limit. */
+    REENCRYPTION_RATE("reencryption_rate_limit", new ReencryptionRateCommand());
+
+    /** Subcommand name. */
+    private final String name;
+
+    /** Command. */
+    private final Command<?> cmd;
+
+    /**
+     * @param name Encryption subcommand name.
+     * @param cmd Command implementation.
+     */
+    EncryptionSubcommands(String name, Command<?> cmd) {
+        this.name = name;
+        this.cmd = cmd;
+    }
+
+    /**
+     * @return Name.
+     */
+    public String text() {
+        return name;
+    }
+
+    /**
+     * @return Cache subcommand implementation.
+     */
+    public Command<?> subcommand() {
+        return cmd;
+    }
+
+    /**
+     * @param text Command text (case insensitive).
+     * @return Command for the text. {@code Null} if there is no such command.
+     */
+     @Nullable public static EncryptionSubcommands of(String text) {
+        for (EncryptionSubcommands cmd : values()) {
+            if (cmd.name.equalsIgnoreCase(text))
+                return cmd;
+        }
+
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return name;
+    }
+}
diff --git a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/GetMasterKeyNameCommand.java b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/GetMasterKeyNameCommand.java
new file mode 100644
index 0000000..02bb8ed
--- /dev/null
+++ b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/GetMasterKeyNameCommand.java
@@ -0,0 +1,73 @@
+/*
+ * 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.commandline.encryption;
+
+import java.util.logging.Logger;
+import org.apache.ignite.internal.client.GridClient;
+import org.apache.ignite.internal.client.GridClientConfiguration;
+import org.apache.ignite.internal.commandline.AbstractCommand;
+import org.apache.ignite.internal.commandline.Command;
+import org.apache.ignite.internal.commandline.CommandLogger;
+import org.apache.ignite.internal.visor.encryption.VisorGetMasterKeyNameTask;
+
+import static org.apache.ignite.internal.commandline.CommandList.ENCRYPTION;
+import static org.apache.ignite.internal.commandline.TaskExecutor.executeTaskByNameOnNode;
+import static org.apache.ignite.internal.commandline.encryption.EncryptionSubcommands.GET_MASTER_KEY_NAME;
+
+/**
+ * Get master key name encryption subcommand.
+ */
+public class GetMasterKeyNameCommand extends AbstractCommand<Void> {
+    /** {@inheritDoc} */
+    @Override public Object execute(GridClientConfiguration clientCfg, Logger log) throws Exception {
+        try (GridClient client = Command.startClient(clientCfg)) {
+            String masterKeyName = executeTaskByNameOnNode(
+                client,
+                VisorGetMasterKeyNameTask.class.getName(),
+                null,
+                null,
+                clientCfg
+            );
+
+            log.info(masterKeyName);
+
+            return masterKeyName;
+        }
+        catch (Throwable e) {
+            log.severe("Failed to perform operation.");
+            log.severe(CommandLogger.errorMessage(e));
+
+            throw e;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public Void arg() {
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void printUsage(Logger log) {
+        Command.usage(log, "Print the current master key name:", ENCRYPTION, GET_MASTER_KEY_NAME.toString());
+    }
+
+    /** {@inheritDoc} */
+    @Override public String name() {
+        return GET_MASTER_KEY_NAME.text().toUpperCase();
+    }
+}
diff --git a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/ReencryptionRateCommand.java b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/ReencryptionRateCommand.java
new file mode 100644
index 0000000..7eb9f79
--- /dev/null
+++ b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/encryption/ReencryptionRateCommand.java
@@ -0,0 +1,137 @@
+/*
+ * 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.commandline.encryption;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.logging.Logger;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.internal.client.GridClient;
+import org.apache.ignite.internal.client.GridClientConfiguration;
+import org.apache.ignite.internal.commandline.AbstractCommand;
+import org.apache.ignite.internal.commandline.Command;
+import org.apache.ignite.internal.commandline.CommandArgIterator;
+import org.apache.ignite.internal.commandline.CommandLogger;
+import org.apache.ignite.internal.visor.encryption.VisorCacheGroupEncryptionTaskResult;
+import org.apache.ignite.internal.visor.encryption.VisorReencryptionRateTask;
+import org.apache.ignite.internal.visor.encryption.VisorReencryptionRateTaskArg;
+
+import static java.util.Collections.singletonMap;
+import static org.apache.ignite.internal.commandline.CommandList.ENCRYPTION;
+import static org.apache.ignite.internal.commandline.CommandLogger.DOUBLE_INDENT;
+import static org.apache.ignite.internal.commandline.CommandLogger.INDENT;
+import static org.apache.ignite.internal.commandline.CommandLogger.optional;
+import static org.apache.ignite.internal.commandline.TaskExecutor.BROADCAST_UUID;
+import static org.apache.ignite.internal.commandline.TaskExecutor.executeTaskByNameOnNode;
+import static org.apache.ignite.internal.commandline.encryption.EncryptionSubcommands.REENCRYPTION_RATE;
+
+/**
+ * View/change cache group re-encryption rate limit subcommand.
+ */
+public class ReencryptionRateCommand extends AbstractCommand<VisorReencryptionRateTaskArg> {
+    /** Re-encryption rate task argument. */
+    private VisorReencryptionRateTaskArg taskArg;
+
+    /** {@inheritDoc} */
+    @Override public Object execute(GridClientConfiguration clientCfg, Logger log) throws Exception {
+        try (GridClient client = Command.startClient(clientCfg)) {
+            VisorCacheGroupEncryptionTaskResult<Double> res = executeTaskByNameOnNode(
+                client,
+                VisorReencryptionRateTask.class.getName(),
+                taskArg,
+                BROADCAST_UUID,
+                clientCfg
+            );
+
+            Map<UUID, IgniteException> exceptions = res.exceptions();
+
+            for (Map.Entry<UUID, IgniteException> entry : exceptions.entrySet()) {
+                log.info(INDENT + "Node " + entry.getKey() + ":");
+                log.info(DOUBLE_INDENT +
+                    "failed to get/set re-encryption rate limit: " + entry.getValue().getMessage());
+            }
+
+            Map<UUID, Double> results = res.results();
+            boolean read = taskArg.rate() == null;
+
+            for (Map.Entry<UUID, Double> entry : results.entrySet()) {
+                log.info(INDENT + "Node " + entry.getKey() + ":");
+
+                double rateLimit = read ? entry.getValue() : taskArg.rate();
+
+                if (rateLimit == 0)
+                    log.info(DOUBLE_INDENT + "re-encryption rate is not limited.");
+                else {
+                    log.info(String.format("%sre-encryption rate %s limited to %.2f MB/s.",
+                        DOUBLE_INDENT, (read ? "is" : "has been"), rateLimit));
+                }
+            }
+
+            if (read)
+                return null;
+
+            log.info("");
+            log.info("Note: the changed value of the re-encryption rate limit is not persisted. " +
+                "When the node is restarted, the value will be set from the configuration.");
+            log.info("");
+
+            return null;
+        }
+        catch (Throwable e) {
+            log.severe("Failed to perform operation.");
+            log.severe(CommandLogger.errorMessage(e));
+
+            throw e;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public VisorReencryptionRateTaskArg arg() {
+        return taskArg;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void parseArguments(CommandArgIterator argIter) {
+        Double rateLimit = null;
+
+        while (argIter.hasNextSubArg()) {
+            String rateLimitArg = argIter.nextArg("Expected decimal value for re-encryption rate.");
+
+            try {
+                rateLimit = Double.parseDouble(rateLimitArg);
+            }
+            catch (NumberFormatException e) {
+                throw new IllegalArgumentException("Failed to parse command argument. Decimal value expected.", e);
+            }
+        }
+
+        taskArg = new VisorReencryptionRateTaskArg(rateLimit);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void printUsage(Logger log) {
+        Command.usage(log, "View/change re-encryption rate limit:", ENCRYPTION,
+            singletonMap("new_limit", "Decimal value to change re-encryption rate limit (MB/s)."),
+            REENCRYPTION_RATE.toString(), optional("new_limit"));
+    }
+
+    /** {@inheritDoc} */
+    @Override public String name() {
+        return REENCRYPTION_RATE.text().toUpperCase();
+    }
+}
diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerAbstractTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerAbstractTest.java
index bc31ce7..8dfbb37 100644
--- a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerAbstractTest.java
+++ b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerAbstractTest.java
@@ -40,6 +40,7 @@ import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.ConnectorConfiguration;
 import org.apache.ignite.configuration.DataRegionConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.EncryptionConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.configuration.WALMode;
 import org.apache.ignite.internal.IgniteEx;
@@ -68,6 +69,8 @@ import static java.util.Arrays.asList;
 import static java.util.Objects.nonNull;
 import static org.apache.ignite.IgniteSystemProperties.IGNITE_ENABLE_EXPERIMENTAL_COMMAND;
 import static org.apache.ignite.configuration.DataStorageConfiguration.DFLT_CHECKPOINT_FREQ;
+import static org.apache.ignite.configuration.EncryptionConfiguration.DFLT_REENCRYPTION_BATCH_SIZE;
+import static org.apache.ignite.configuration.EncryptionConfiguration.DFLT_REENCRYPTION_RATE_MBPS;
 import static org.apache.ignite.internal.encryption.AbstractEncryptionTest.KEYSTORE_PASSWORD;
 import static org.apache.ignite.internal.encryption.AbstractEncryptionTest.KEYSTORE_PATH;
 import static org.apache.ignite.internal.processors.cache.verify.VerifyBackupPartitionsDumpTask.IDLE_DUMP_FILE_PREFIX;
@@ -85,6 +88,9 @@ public abstract class GridCommandHandlerAbstractTest extends GridCommonAbstractT
     /** */
     protected static final String CLIENT_NODE_NAME_PREFIX = "client";
 
+    /** */
+    protected static final String DAEMON_NODE_NAME_PREFIX = "daemon";
+
     /** Option is used for auto confirmation. */
     protected static final String CMD_AUTO_CONFIRMATION = "--yes";
 
@@ -113,7 +119,13 @@ public abstract class GridCommandHandlerAbstractTest extends GridCommonAbstractT
     protected boolean autoConfirmation = true;
 
     /** {@code True} if encription is enabled. */
-    protected boolean encriptionEnabled;
+    protected boolean encryptionEnabled;
+
+    /**  Re-encryption rate limit in megabytes per second. */
+    protected double reencryptSpeed = DFLT_REENCRYPTION_RATE_MBPS;
+
+    /** The number of pages that is scanned during re-encryption under checkpoint lock. */
+    protected int reencryptBatchSize = DFLT_REENCRYPTION_BATCH_SIZE;
 
     /** Last operation result. */
     protected Object lastOperationResult;
@@ -171,7 +183,7 @@ public abstract class GridCommandHandlerAbstractTest extends GridCommonAbstractT
 
         testOut.reset();
 
-        encriptionEnabled = false;
+        encryptionEnabled = false;
 
         GridClientFactory.stopAll(false);
     }
@@ -233,13 +245,22 @@ public abstract class GridCommandHandlerAbstractTest extends GridCommonAbstractT
 
         cfg.setClientMode(igniteInstanceName.startsWith(CLIENT_NODE_NAME_PREFIX));
 
-        if (encriptionEnabled) {
+        cfg.setDaemon(igniteInstanceName.startsWith(DAEMON_NODE_NAME_PREFIX));
+
+        if (encryptionEnabled) {
             KeystoreEncryptionSpi encSpi = new KeystoreEncryptionSpi();
 
             encSpi.setKeyStorePath(KEYSTORE_PATH);
             encSpi.setKeyStorePassword(KEYSTORE_PASSWORD.toCharArray());
 
             cfg.setEncryptionSpi(encSpi);
+
+            EncryptionConfiguration encCfg = new EncryptionConfiguration();
+
+            encCfg.setReencryptionRateLimit(reencryptSpeed);
+            encCfg.setReencryptionBatchSize(reencryptBatchSize);
+
+            dsCfg.setEncryptionConfiguration(encCfg);
         }
 
         return cfg;
@@ -418,7 +439,8 @@ public abstract class GridCommandHandlerAbstractTest extends GridCommonAbstractT
 
         CacheConfiguration<?, ?> ccfg = new CacheConfiguration<>(DEFAULT_CACHE_NAME)
             .setAffinity(new RendezvousAffinityFunction(false, partitions))
-            .setBackups(1);
+            .setBackups(1)
+            .setEncryptionEnabled(encryptionEnabled);
 
         if (filter != null)
             ccfg.setNodeFilter(filter);
diff --git a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerTest.java b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerTest.java
index cd22973..ec1508d 100644
--- a/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerTest.java
+++ b/modules/control-utility/src/test/java/org/apache/ignite/util/GridCommandHandlerTest.java
@@ -102,8 +102,10 @@ import org.apache.ignite.internal.processors.cache.warmup.BlockedWarmUpStrategy;
 import org.apache.ignite.internal.processors.cache.warmup.WarmUpTestPluginProvider;
 import org.apache.ignite.internal.processors.cluster.GridClusterStateProcessor;
 import org.apache.ignite.internal.util.lang.GridAbsPredicate;
+import org.apache.ignite.internal.util.lang.GridFunc;
 import org.apache.ignite.internal.util.typedef.G;
 import org.apache.ignite.internal.util.typedef.X;
+import org.apache.ignite.internal.util.typedef.internal.CU;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.internal.visor.cache.VisorFindAndDeleteGarbageInPersistenceTaskResult;
 import org.apache.ignite.internal.visor.tx.VisorTxInfo;
@@ -140,6 +142,12 @@ import static org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_IN
 import static org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_OK;
 import static org.apache.ignite.internal.commandline.CommandHandler.EXIT_CODE_UNEXPECTED_ERROR;
 import static org.apache.ignite.internal.commandline.CommandList.DEACTIVATE;
+import static org.apache.ignite.internal.commandline.encryption.EncryptionSubcommands.CACHE_GROUP_KEY_IDS;
+import static org.apache.ignite.internal.commandline.encryption.EncryptionSubcommands.CHANGE_CACHE_GROUP_KEY;
+import static org.apache.ignite.internal.commandline.encryption.EncryptionSubcommands.REENCRYPTION_RATE;
+import static org.apache.ignite.internal.commandline.encryption.EncryptionSubcommands.REENCRYPTION_RESUME;
+import static org.apache.ignite.internal.commandline.encryption.EncryptionSubcommands.REENCRYPTION_STATUS;
+import static org.apache.ignite.internal.commandline.encryption.EncryptionSubcommands.REENCRYPTION_SUSPEND;
 import static org.apache.ignite.internal.encryption.AbstractEncryptionTest.MASTER_KEY_NAME_2;
 import static org.apache.ignite.internal.processors.cache.persistence.GridCacheDatabaseSharedManager.IGNITE_PDS_SKIP_CHECKPOINT_ON_NODE_STOP;
 import static org.apache.ignite.internal.processors.cache.persistence.snapshot.AbstractSnapshotSelfTest.doSnapshotCancellationTest;
@@ -2630,7 +2638,7 @@ public class GridCommandHandlerTest extends GridCommandHandlerClusterPerMethodAb
     /** @throws Exception If failed. */
     @Test
     public void testMasterKeyChange() throws Exception {
-        encriptionEnabled = true;
+        encryptionEnabled = true;
 
         injectTestSystemOut();
 
@@ -2667,8 +2675,187 @@ public class GridCommandHandlerTest extends GridCommandHandlerClusterPerMethodAb
 
     /** @throws Exception If failed. */
     @Test
+    public void testCacheGroupKeyChange() throws Exception {
+        encryptionEnabled = true;
+
+        injectTestSystemOut();
+
+        int srvNodes = 2;
+
+        IgniteEx ignite = startGrids(srvNodes);
+
+        startGrid(CLIENT_NODE_NAME_PREFIX);
+        startGrid(DAEMON_NODE_NAME_PREFIX);
+
+        ignite.cluster().state(ACTIVE);
+
+        List<Ignite> srvGrids = GridFunc.asList(grid(0), grid(1));
+
+        enableCheckpoints(srvGrids, false);
+
+        createCacheAndPreload(ignite, 1000);
+
+        int ret = execute("--encryption", CACHE_GROUP_KEY_IDS.toString(), DEFAULT_CACHE_NAME);
+
+        assertEquals(EXIT_CODE_OK, ret);
+        assertContains(log, testOut.toString(), "Encryption key identifiers for cache: " + DEFAULT_CACHE_NAME);
+        assertEquals(srvNodes, countSubstrs(testOut.toString(), "0 (active)"));
+
+        ret = execute("--encryption", CHANGE_CACHE_GROUP_KEY.toString(), DEFAULT_CACHE_NAME);
+
+        assertEquals(EXIT_CODE_OK, ret);
+        assertContains(log, testOut.toString(),
+            "The encryption key has been changed for the cache group \"" + DEFAULT_CACHE_NAME + '"');
+
+        ret = execute("--encryption", CACHE_GROUP_KEY_IDS.toString(), DEFAULT_CACHE_NAME);
+
+        assertEquals(testOut.toString(), EXIT_CODE_OK, ret);
+        assertContains(log, testOut.toString(), "Encryption key identifiers for cache: " + DEFAULT_CACHE_NAME);
+        assertEquals(srvNodes, countSubstrs(testOut.toString(), "1 (active)"));
+
+        GridTestUtils.waitForCondition(() -> {
+            execute("--encryption", REENCRYPTION_STATUS.toString(), DEFAULT_CACHE_NAME);
+
+            return srvNodes == countSubstrs(testOut.toString(),
+                "re-encryption will be completed after the next checkpoint");
+        }, getTestTimeout());
+
+        enableCheckpoints(srvGrids, true);
+        forceCheckpoint(srvGrids);
+
+        GridTestUtils.waitForCondition(() -> {
+            execute("--encryption", REENCRYPTION_STATUS.toString(), DEFAULT_CACHE_NAME);
+
+            return srvNodes == countSubstrs(testOut.toString(), "re-encryption completed or not required");
+        }, getTestTimeout());
+    }
+
+    /** @throws Exception If failed. */
+    @Test
+    public void testChangeReencryptionRate() throws Exception {
+        int srvNodes = 2;
+
+        IgniteEx ignite = startGrids(srvNodes);
+
+        ignite.cluster().state(ACTIVE);
+
+        injectTestSystemOut();
+
+        int ret = execute("--encryption", REENCRYPTION_RATE.toString());
+
+        assertEquals(EXIT_CODE_OK, ret);
+        assertEquals(srvNodes, countSubstrs(testOut.toString(), "re-encryption rate is not limited."));
+
+        double newRate = 0.01;
+
+        ret = execute("--encryption", REENCRYPTION_RATE.toString(), Double.toString(newRate));
+
+        assertEquals(EXIT_CODE_OK, ret);
+        assertEquals(srvNodes, countSubstrs(testOut.toString(),
+            String.format("re-encryption rate has been limited to %.2f MB/s.", newRate)));
+
+        ret = execute("--encryption", REENCRYPTION_RATE.toString());
+
+        assertEquals(EXIT_CODE_OK, ret);
+        assertEquals(srvNodes, countSubstrs(testOut.toString(),
+            String.format("re-encryption rate is limited to %.2f MB/s.", newRate)));
+
+        ret = execute("--encryption", REENCRYPTION_RATE.toString(), "0");
+
+        assertEquals(EXIT_CODE_OK, ret);
+        assertEquals(srvNodes, countSubstrs(testOut.toString(), "re-encryption rate is not limited."));
+    }
+
+    /** @throws Exception If failed. */
+    @Test
+    public void testReencryptionSuspendAndResume() throws Exception {
+        encryptionEnabled = true;
+        reencryptSpeed = 0.01;
+        reencryptBatchSize = 1;
+
+        int srvNodes = 2;
+
+        IgniteEx ignite = startGrids(srvNodes);
+
+        ignite.cluster().state(ACTIVE);
+
+        injectTestSystemOut();
+
+        createCacheAndPreload(ignite, 10_000);
+
+        ignite.encryption().changeCacheGroupKey(Collections.singleton(DEFAULT_CACHE_NAME)).get();
+
+        assertTrue(isReencryptionStarted(DEFAULT_CACHE_NAME));
+
+        int ret = execute("--encryption", REENCRYPTION_STATUS.toString(), DEFAULT_CACHE_NAME);
+
+        assertEquals(EXIT_CODE_OK, ret);
+
+        Pattern ptrn = Pattern.compile("(?m)Node [-0-9a-f]{36}:\n\\s+(?<left>\\d+) KB of data.+");
+        Matcher matcher = ptrn.matcher(testOut.toString());
+        int matchesCnt = 0;
+
+        while (matcher.find()) {
+            assertEquals(1, matcher.groupCount());
+
+            int pagesLeft = Integer.parseInt(matcher.group("left"));
+
+            assertTrue(pagesLeft > 0);
+
+            matchesCnt++;
+        }
+
+        assertEquals(srvNodes, matchesCnt);
+
+        ret = execute("--encryption", REENCRYPTION_SUSPEND.toString(), DEFAULT_CACHE_NAME);
+
+        assertEquals(EXIT_CODE_OK, ret);
+        assertEquals(srvNodes, countSubstrs(testOut.toString(),
+            "re-encryption of the cache group \"" + DEFAULT_CACHE_NAME + "\" has been suspended."));
+        assertFalse(isReencryptionStarted(DEFAULT_CACHE_NAME));
+
+        ret = execute("--encryption", REENCRYPTION_SUSPEND.toString(), DEFAULT_CACHE_NAME);
+
+        assertEquals(EXIT_CODE_OK, ret);
+        assertEquals(srvNodes, countSubstrs(testOut.toString(),
+            "re-encryption of the cache group \"" + DEFAULT_CACHE_NAME + "\" has already been suspended."));
+
+        ret = execute("--encryption", REENCRYPTION_RESUME.toString(), DEFAULT_CACHE_NAME);
+
+        assertEquals(EXIT_CODE_OK, ret);
+        assertEquals(srvNodes, countSubstrs(testOut.toString(),
+            "re-encryption of the cache group \"" + DEFAULT_CACHE_NAME + "\" has been resumed."));
+        assertTrue(isReencryptionStarted(DEFAULT_CACHE_NAME));
+
+        ret = execute("--encryption", REENCRYPTION_RESUME.toString(), DEFAULT_CACHE_NAME);
+
+        assertEquals(EXIT_CODE_OK, ret);
+        assertEquals(srvNodes, countSubstrs(testOut.toString(),
+            "re-encryption of the cache group \"" + DEFAULT_CACHE_NAME + "\" has already been resumed."));
+    }
+
+    /**
+     * @param cacheName Cache name.
+     * @return {@code True} if re-encryption of the specified cache is started on all server nodes.
+     */
+    private boolean isReencryptionStarted(String cacheName) {
+        for (Ignite grid : G.allGrids()) {
+            ClusterNode locNode = grid.cluster().localNode();
+
+            if (locNode.isClient() || locNode.isDaemon())
+                continue;
+
+            if (((IgniteEx)grid).context().encryption().reencryptionFuture(CU.cacheId(cacheName)).isDone())
+                return false;
+        }
+
+        return true;
+    }
+
+    /** @throws Exception If failed. */
+    @Test
     public void testMasterKeyChangeOnInactiveCluster() throws Exception {
-        encriptionEnabled = true;
+        encryptionEnabled = true;
 
         injectTestSystemOut();
 
@@ -2838,4 +3025,18 @@ public class GridCommandHandlerTest extends GridCommandHandlerClusterPerMethodAb
 
         return hnd.getLastOperationResult();
     }
+
+    /**
+     * @param str String.
+     * @param substr Substring to find in the specified string.
+     * @return The number of substrings found in the specified string.
+     */
+    private int countSubstrs(String str, String substr) {
+        int cnt = 0;
+
+        for (int off = 0; (off = str.indexOf(substr, off)) != -1; off++)
+            ++cnt;
+
+        return cnt;
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/configuration/EncryptionConfiguration.java b/modules/core/src/main/java/org/apache/ignite/configuration/EncryptionConfiguration.java
index 79e205e..6b9345d 100644
--- a/modules/core/src/main/java/org/apache/ignite/configuration/EncryptionConfiguration.java
+++ b/modules/core/src/main/java/org/apache/ignite/configuration/EncryptionConfiguration.java
@@ -75,7 +75,7 @@ public class EncryptionConfiguration implements Serializable {
      */
     public EncryptionConfiguration setReencryptionRateLimit(double reencryptionRateLimit) {
         A.ensure(reencryptionRateLimit >= 0,
-            "Reencryption rate limit (" + reencryptionRateLimit + ") must be non-negative.");
+            "Re-encryption rate limit (" + reencryptionRateLimit + ") must be non-negative.");
 
         this.reencryptionRateLimit = reencryptionRateLimit;
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/managers/encryption/CacheGroupPageScanner.java b/modules/core/src/main/java/org/apache/ignite/internal/managers/encryption/CacheGroupPageScanner.java
index dc0a29b..c2a6895 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/managers/encryption/CacheGroupPageScanner.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/managers/encryption/CacheGroupPageScanner.java
@@ -25,6 +25,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.locks.ReentrantLock;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteLogger;
@@ -77,15 +78,15 @@ public class CacheGroupPageScanner implements CheckpointListener {
     /** Collection of groups waiting for a checkpoint. */
     private final Collection<GroupScanTask> cpWaitGrps = new ConcurrentLinkedQueue<>();
 
-    /** Page scanning speed limiter. */
-    private final BasicRateLimiter limiter;
-
     /** Single-threaded executor to run cache group scan task. */
     private final ThreadPoolExecutor singleExecSvc;
 
     /** Number of pages that is scanned during reencryption under checkpoint lock. */
     private final int batchSize;
 
+    /** Page scanning speed limiter. */
+    private final BasicRateLimiter limiter;
+
     /** Stop flag. */
     private boolean stopped;
 
@@ -109,8 +110,7 @@ public class CacheGroupPageScanner implements CheckpointListener {
 
         double rateLimit = dsCfg.getEncryptionConfiguration().getReencryptionRateLimit();
 
-        limiter = rateLimit > 0 ? new BasicRateLimiter(rateLimit * MB /
-            (dsCfg.getPageSize() == 0 ? DataStorageConfiguration.DFLT_PAGE_SIZE : dsCfg.getPageSize())) : null;
+        limiter = new BasicRateLimiter(calcPermits(rateLimit, dsCfg));
 
         batchSize = dsCfg.getEncryptionConfiguration().getReencryptionBatchSize();
 
@@ -210,10 +210,13 @@ public class CacheGroupPageScanner implements CheckpointListener {
             }
 
             Set<Integer> parts = new HashSet<>();
+            long[] pagesLeft = new long[1];
 
             forEachPageStore(grp, new IgniteInClosureX<Integer>() {
                 @Override public void applyx(Integer partId) {
-                    if (ctx.encryption().getEncryptionState(grpId, partId) == 0) {
+                    long encState = ctx.encryption().getEncryptionState(grpId, partId);
+
+                    if (encState == 0) {
                         if (log.isDebugEnabled())
                             log.debug("Skipping partition reencryption [grp=" + grpId + ", p=" + partId + "]");
 
@@ -221,10 +224,12 @@ public class CacheGroupPageScanner implements CheckpointListener {
                     }
 
                     parts.add(partId);
+
+                    pagesLeft[0] += (ReencryptStateUtils.pageCount(encState) - ReencryptStateUtils.pageIndex(encState));
                 }
             });
 
-            GroupScanTask grpScan = new GroupScanTask(grp, parts);
+            GroupScanTask grpScan = new GroupScanTask(grp, parts, pagesLeft[0]);
 
             singleExecSvc.submit(grpScan);
 
@@ -314,6 +319,51 @@ public class CacheGroupPageScanner implements CheckpointListener {
     }
 
     /**
+     * @param grpId Cache group ID.
+     * @return Number of remaining memory pages to scan.
+     */
+    public long remainingPagesCount(int grpId) {
+        GroupScanTask grpScanTask = grps.get(grpId);
+
+        if (grpScanTask != null)
+            return grpScanTask.remainingPagesCount();
+
+        return 0;
+    }
+
+    /**
+     * @return Re-encryption rate limit in megabytes per second ({@code 0} - unlimited).
+     */
+    public double getRate() {
+        DataStorageConfiguration dsCfg = ctx.config().getDataStorageConfiguration();
+
+        if (CU.isPersistenceEnabled(dsCfg))
+            return dsCfg.getPageSize() * limiter.getRate() / MB;
+
+        return 0;
+    }
+
+    /**
+     * @param rate Re-encryption rate limit in megabytes per second ({@code 0} - unlimited).
+     */
+    public void setRate(double rate) {
+        DataStorageConfiguration dsCfg = ctx.config().getDataStorageConfiguration();
+
+        if (CU.isPersistenceEnabled(dsCfg))
+            limiter.setRate(calcPermits(rate, dsCfg));
+    }
+
+    /**
+     * @param rate Maximum scan speed in megabytes per second
+     * @param dsCfg Datastorage configuration.
+     * @return The number of permits allowed per second.
+     */
+    private double calcPermits(double rate, DataStorageConfiguration dsCfg) {
+        return rate * MB /
+            (dsCfg.getPageSize() == 0 ? DataStorageConfiguration.DFLT_PAGE_SIZE : dsCfg.getPageSize());
+    }
+
+    /**
      * @param grp Cache group.
      * @param hnd Partition handler.
      */
@@ -345,13 +395,17 @@ public class CacheGroupPageScanner implements CheckpointListener {
         /** Page memory. */
         private final PageMemoryEx pageMem;
 
+        /** Total memory pages left for reencryption. */
+        private final AtomicLong remainingPagesCntr;
+
         /**
          * @param grp Cache group.
          */
-        public GroupScanTask(CacheGroupContext grp, Set<Integer> parts) {
+        public GroupScanTask(CacheGroupContext grp, Set<Integer> parts, long remainingPagesCnt) {
             this.grp = grp;
             this.parts = new GridConcurrentHashSet<>(parts);
 
+            remainingPagesCntr = new AtomicLong(remainingPagesCnt);
             pageMem = (PageMemoryEx)grp.dataRegion().pageMemory();
         }
 
@@ -367,6 +421,10 @@ public class CacheGroupPageScanner implements CheckpointListener {
          * @return {@code True} if reencryption was cancelled.
          */
         public synchronized boolean excludePartition(int partId) {
+            long state = ctx.encryption().getEncryptionState(groupId(), partId);
+
+            remainingPagesCntr.addAndGet(ReencryptStateUtils.pageIndex(state) - ReencryptStateUtils.pageCount(state));
+
             return parts.remove(partId);
         }
 
@@ -377,6 +435,13 @@ public class CacheGroupPageScanner implements CheckpointListener {
             return grp.groupId();
         }
 
+        /**
+         * @return Number of remaining memory pages to scan.
+         */
+        public long remainingPagesCount() {
+            return remainingPagesCntr.get();
+        }
+
         /** {@inheritDoc} */
         @Override public void run() {
             try {
@@ -418,8 +483,7 @@ public class CacheGroupPageScanner implements CheckpointListener {
             while (off < cnt) {
                 int pagesCnt = Math.min(batchSize, cnt - off);
 
-                if (limiter != null)
-                    limiter.acquire(pagesCnt);
+                limiter.acquire(pagesCnt);
 
                 synchronized (this) {
                     if (isDone() || !parts.contains(partId))
@@ -435,6 +499,8 @@ public class CacheGroupPageScanner implements CheckpointListener {
                     }
                 }
 
+                remainingPagesCntr.addAndGet(-pagesCnt);
+
                 ctx.encryption().setEncryptionState(grp, partId, off, cnt);
             }
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/managers/encryption/GridEncryptionManager.java b/modules/core/src/main/java/org/apache/ignite/internal/managers/encryption/GridEncryptionManager.java
index 2c31dcd..d0d467f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/managers/encryption/GridEncryptionManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/managers/encryption/GridEncryptionManager.java
@@ -824,6 +824,20 @@ public class GridEncryptionManager extends GridManagerAdapter<EncryptionSpi> imp
     }
 
     /**
+     * @return Re-encryption rate limit in megabytes per second ({@code 0} - unlimited).
+     */
+    public double getReencryptionRate() {
+        return pageScanner.getRate();
+    }
+
+    /**
+     * @param rate Re-encryption rate limit in megabytes per second ({@code 0} - unlimited).
+     */
+    public void setReencryptionRate(double rate) {
+        pageScanner.setRate(rate);
+    }
+
+    /**
      * Removes encryption key(s).
      *
      * @param grpId Cache group ID.
@@ -1116,6 +1130,14 @@ public class GridEncryptionManager extends GridManagerAdapter<EncryptionSpi> imp
     }
 
     /**
+     * @param grpId Cache group ID.
+     * @return The number of bytes left for re-ecryption.
+     */
+    public long getBytesLeftForReencryption(int grpId) {
+        return pageScanner.remainingPagesCount(grpId) * ctx.config().getDataStorageConfiguration().getPageSize();
+    }
+
+    /**
      * @param keyCnt Count of keys to generate.
      * @return Future that will contain results of generation.
      */
@@ -1160,6 +1182,32 @@ public class GridEncryptionManager extends GridManagerAdapter<EncryptionSpi> imp
     }
 
     /**
+     * Suspend re-encryption of the cache group.
+     *
+     * @param grpId Cache group ID.
+     */
+    public boolean suspendReencryption(int grpId) throws IgniteCheckedException {
+        return reencryptionFuture(grpId).cancel();
+    }
+
+    /**
+     * Forces re-encryption of the cache group.
+     *
+     * @param grpId Cache group ID.
+     */
+    public boolean resumeReencryption(int grpId) throws IgniteCheckedException {
+        if (!reencryptionFuture(grpId).isDone())
+            return false;
+
+        if (!reencryptionInProgress(grpId))
+            throw new IgniteCheckedException("Re-encryption completed or not required [grpId=" + grpId + "]");
+
+        startReencryption(Collections.singleton(grpId));
+
+        return true;
+    }
+
+    /**
      * @param grpIds Cache group IDs.
      * @throws IgniteCheckedException If failed.
      */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheGroupMetricsImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheGroupMetricsImpl.java
index 75fdd15..b7c0300 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheGroupMetricsImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheGroupMetricsImpl.java
@@ -25,15 +25,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
-import org.apache.ignite.IgniteCheckedException;
-import org.apache.ignite.IgniteException;
 import org.apache.ignite.cache.CacheMode;
 import org.apache.ignite.cluster.ClusterNode;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.DataStorageConfiguration;
-import org.apache.ignite.internal.managers.encryption.GridEncryptionManager;
-import org.apache.ignite.internal.managers.encryption.ReencryptStateUtils;
-import org.apache.ignite.internal.pagemem.PageIdAllocator;
 import org.apache.ignite.internal.pagemem.store.PageStore;
 import org.apache.ignite.internal.processors.affinity.AffinityAssignment;
 import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
@@ -44,7 +39,6 @@ import org.apache.ignite.internal.processors.cache.distributed.dht.topology.Grid
 import org.apache.ignite.internal.processors.cache.persistence.DataRegion;
 import org.apache.ignite.internal.processors.cache.persistence.DataRegionMetricsImpl;
 import org.apache.ignite.internal.processors.cache.persistence.GridCacheDatabaseSharedManager;
-import org.apache.ignite.internal.processors.cache.persistence.file.FilePageStoreManager;
 import org.apache.ignite.internal.processors.metric.MetricRegistry;
 import org.apache.ignite.internal.processors.metric.impl.AtomicLongMetric;
 import org.apache.ignite.internal.processors.metric.impl.LongAdderMetric;
@@ -187,9 +181,9 @@ public class CacheGroupMetricsImpl {
                 () -> !ctx.shared().kernalContext().encryption().reencryptionInProgress(ctx.groupId()),
                 "The flag indicates whether reencryption is finished or not.");
 
-            mreg.register("ReencryptionPagesLeft",
-                this::getPagesLeftForReencryption,
-                "Number of pages left for reencryption.");
+            mreg.register("ReencryptionBytesLeft",
+                () -> ctx.shared().kernalContext().encryption().getBytesLeftForReencryption(ctx.groupId()),
+                "The number of bytes left for re-ecryption.");
         }
     }
 
@@ -502,40 +496,6 @@ public class CacheGroupMetricsImpl {
         return sparseStorageSize == null ? 0 : sparseStorageSize.value();
     }
 
-    /** */
-    public long getPagesLeftForReencryption() {
-        if (!ctx.shared().kernalContext().encryption().reencryptionInProgress(ctx.groupId()))
-            return 0;
-
-        long pagesLeft = 0;
-
-        FilePageStoreManager mgr = (FilePageStoreManager)ctx.shared().pageStore();
-
-        GridEncryptionManager encMgr = ctx.shared().kernalContext().encryption();
-
-        try {
-            for (int p = 0; p < ctx.affinity().partitions(); p++) {
-                PageStore pageStore = mgr.getStore(ctx.groupId(), p);
-
-                if (!pageStore.exists())
-                    continue;
-
-                long state = encMgr.getEncryptionState(ctx.groupId(), p);
-
-                pagesLeft += ReencryptStateUtils.pageCount(state) - ReencryptStateUtils.pageIndex(state);
-            }
-
-            long state = encMgr.getEncryptionState(ctx.groupId(), PageIdAllocator.INDEX_PARTITION);
-
-            pagesLeft += ReencryptStateUtils.pageCount(state) - ReencryptStateUtils.pageIndex(state);
-        }
-        catch (IgniteCheckedException e) {
-            throw new IgniteException(e);
-        }
-
-        return pagesLeft;
-    }
-
     /** Removes all metric for cache group. */
     public void remove() {
         if (ctx.shared().kernalContext().isStopping())
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/BasicRateLimiter.java b/modules/core/src/main/java/org/apache/ignite/internal/util/BasicRateLimiter.java
index 8429ccb..195264e 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/BasicRateLimiter.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/BasicRateLimiter.java
@@ -65,6 +65,11 @@ public class BasicRateLimiter {
     private long nextFreeTicketMicros;
 
     /**
+     * The flag indicates that the rate is not limited.
+     */
+    private volatile boolean unlimited;
+
+    /**
      * @param permitsPerSecond Estimated number of permits per second.
      */
     public BasicRateLimiter(double permitsPerSecond) {
@@ -74,11 +79,14 @@ public class BasicRateLimiter {
     /**
      * Updates the stable rate.
      *
-     * @param permitsPerSecond The new stable rate of this {@code RateLimiter}.
+     * @param permitsPerSecond The new stable rate of this {@code RateLimiter}, set {@code 0} for unlimited rate.
      * @throws IllegalArgumentException If {@code permitsPerSecond} is negative or zero.
      */
     public void setRate(double permitsPerSecond) {
-        A.ensure(permitsPerSecond > 0, "Requested permits (" + permitsPerSecond + ") must be positive");
+        A.ensure(permitsPerSecond >= 0, "Requested permits (" + permitsPerSecond + ") must be non-negative.");
+
+        if (unlimited = (permitsPerSecond == 0))
+            return;
 
         synchronized (mux) {
             resync();
@@ -88,9 +96,12 @@ public class BasicRateLimiter {
     }
 
     /**
-     * @return The stable rate (as {@code permits per seconds}).
+     * @return The stable rate as {@code permits per seconds} ({@code 0} means that the rate is unlimited).
      */
     public double getRate() {
+        if (unlimited)
+            return 0;
+
         synchronized (mux) {
             return SECONDS.toMicros(1L) / stableIntervalMicros;
         }
@@ -104,6 +115,9 @@ public class BasicRateLimiter {
      * @throws IllegalArgumentException If the requested number of permits is negative or zero.
      */
     public void acquire(int permits) throws IgniteInterruptedCheckedException {
+        if (unlimited)
+            return;
+
         long microsToWait = reserve(permits);
 
         try {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorCacheGroupEncryptionTask.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorCacheGroupEncryptionTask.java
new file mode 100644
index 0000000..37d5096
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorCacheGroupEncryptionTask.java
@@ -0,0 +1,137 @@
+/*
+ * 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.visor.encryption;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.compute.ComputeJobResult;
+import org.apache.ignite.internal.dto.IgniteDataTransferObject;
+import org.apache.ignite.internal.processors.cache.CacheGroupContext;
+import org.apache.ignite.internal.processors.cache.IgniteInternalCache;
+import org.apache.ignite.internal.util.typedef.internal.CU;
+import org.apache.ignite.internal.visor.VisorJob;
+import org.apache.ignite.internal.visor.VisorMultiNodeTask;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Visor encrypted cache group multinode task.
+ *
+ * @param <T> The type of the task result.
+ */
+public abstract class VisorCacheGroupEncryptionTask<T> extends VisorMultiNodeTask<VisorCacheGroupEncryptionTaskArg,
+    VisorCacheGroupEncryptionTaskResult<T>, VisorCacheGroupEncryptionTask.VisorSingleFieldDto<T>>
+{
+    /** {@inheritDoc} */
+    @Nullable @Override protected VisorCacheGroupEncryptionTaskResult<T> reduce0(List<ComputeJobResult> results) {
+        Map<UUID, T> jobResults = new HashMap<>();
+        Map<UUID, IgniteException> exceptions = new HashMap<>();
+
+        for (ComputeJobResult res : results) {
+            UUID nodeId = res.getNode().id();
+
+            if (res.getException() != null) {
+                exceptions.put(nodeId, res.getException());
+
+                continue;
+            }
+
+            VisorSingleFieldDto<T> dtoRes = res.getData();
+
+            jobResults.put(nodeId, dtoRes.value());
+        }
+
+        return new VisorCacheGroupEncryptionTaskResult<>(jobResults, exceptions);
+    }
+
+    /** */
+    protected abstract static class VisorSingleFieldDto<T> extends IgniteDataTransferObject {
+        /** Object value. */
+        private T val;
+
+        /**
+          * @return Object value.
+         */
+        protected T value() {
+            return val;
+        }
+
+        /**
+         * @param val Data object.
+         * @return {@code this} for chaining.
+         */
+        protected VisorSingleFieldDto<T> value(T val) {
+            this.val = val;
+
+            return this;
+        }
+    }
+
+    /**
+     * @param <T> Type of job result.
+     */
+    protected abstract static class VisorReencryptionBaseJob<T>
+        extends VisorJob<VisorCacheGroupEncryptionTaskArg, VisorSingleFieldDto<T>> {
+        /**
+         * @param arg Job argument.
+         * @param debug Flag indicating whether debug information should be printed into node log.
+         */
+        protected VisorReencryptionBaseJob(@Nullable VisorCacheGroupEncryptionTaskArg arg, boolean debug) {
+            super(arg, debug);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected VisorSingleFieldDto<T> run(VisorCacheGroupEncryptionTaskArg arg) throws IgniteException {
+            try {
+                String grpName = arg.groupName();
+                CacheGroupContext grp = ignite.context().cache().cacheGroup(CU.cacheId(grpName));
+
+                if (grp == null) {
+                    IgniteInternalCache<Object, Object> cache = ignite.context().cache().cache(grpName);
+
+                    if (cache == null)
+                        throw new IgniteException("Cache group " + grpName + " not found.");
+
+                    grp = cache.context().group();
+
+                    if (grp.sharedGroup()) {
+                        throw new IgniteException("Cache or group \"" + grpName + "\" is a part of group \"" +
+                            grp.name() + "\". Provide group name instead of cache name for shared groups.");
+                    }
+                }
+
+                return run0(grp);
+            }
+            catch (IgniteCheckedException e) {
+                throw new IgniteException(e);
+            }
+        }
+
+        /**
+         * Executes internal logic of the job.
+         *
+         * @param grp Cache group.
+         * @return Result.
+         * @throws IgniteCheckedException In case of error.
+         */
+        protected abstract VisorSingleFieldDto<T> run0(CacheGroupContext grp) throws IgniteCheckedException;
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorCacheGroupEncryptionTaskArg.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorCacheGroupEncryptionTaskArg.java
new file mode 100644
index 0000000..ecea9ed
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorCacheGroupEncryptionTaskArg.java
@@ -0,0 +1,68 @@
+/*
+ * 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.visor.encryption;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import org.apache.ignite.internal.dto.IgniteDataTransferObject;
+import org.apache.ignite.internal.util.typedef.internal.S;
+import org.apache.ignite.internal.util.typedef.internal.U;
+
+/**
+ * Cache group aware task argument.
+ */
+public class VisorCacheGroupEncryptionTaskArg extends IgniteDataTransferObject {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Cache group name. */
+    private String grpName;
+
+    /** Default constructor. */
+    public VisorCacheGroupEncryptionTaskArg() {
+        // No-op.
+    }
+
+    /**
+     * @param grpName Cache group name.
+     */
+    public VisorCacheGroupEncryptionTaskArg(String grpName) {
+        this.grpName = grpName;
+    }
+
+    /** @return Cache group name. */
+    public String groupName() {
+        return grpName;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void writeExternalData(ObjectOutput out) throws IOException {
+        U.writeString(out, grpName);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void readExternalData(byte ver, ObjectInput in) throws IOException, ClassNotFoundException {
+        grpName = U.readString(in);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(VisorCacheGroupEncryptionTaskArg.class, this);
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorCacheGroupEncryptionTaskResult.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorCacheGroupEncryptionTaskResult.java
new file mode 100644
index 0000000..f29a99b
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorCacheGroupEncryptionTaskResult.java
@@ -0,0 +1,90 @@
+/*
+ * 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.visor.encryption;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.Collections;
+import java.util.Map;
+import java.util.UUID;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.internal.dto.IgniteDataTransferObject;
+import org.apache.ignite.internal.util.tostring.GridToStringInclude;
+import org.apache.ignite.internal.util.typedef.internal.S;
+import org.apache.ignite.internal.util.typedef.internal.U;
+
+/**
+ * Multinode cache group encryption task result.
+ *
+ * @param <T> Job result type.
+ */
+@SuppressWarnings("AssignmentOrReturnOfFieldWithMutableType")
+public class VisorCacheGroupEncryptionTaskResult<T> extends IgniteDataTransferObject {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Per node job result. */
+    @GridToStringInclude
+    private Map<UUID, T> results;
+
+    /** Per node execution problems. */
+    @GridToStringInclude
+    private Map<UUID, IgniteException> exceptions;
+
+    /**
+     * @param results Per node job result.
+     * @param exceptions Per node execution problems.
+     */
+    public VisorCacheGroupEncryptionTaskResult(Map<UUID, T> results, Map<UUID, IgniteException> exceptions) {
+        this.results = results;
+        this.exceptions = exceptions;
+    }
+
+    /** */
+    public VisorCacheGroupEncryptionTaskResult() {
+        // No-op.
+    }
+
+    /** @return Per node job result. */
+    public Map<UUID, T> results() {
+        return results == null ? Collections.emptyMap() : results;
+    }
+
+    /** @return Per node execution problems. */
+    public Map<UUID, IgniteException> exceptions() {
+        return exceptions == null ? Collections.emptyMap() : exceptions;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void writeExternalData(ObjectOutput out) throws IOException {
+        U.writeMap(out, results);
+        U.writeMap(out, exceptions);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void readExternalData(byte ver, ObjectInput in) throws IOException, ClassNotFoundException {
+        results = U.readMap(in);
+        exceptions = U.readMap(in);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(VisorCacheGroupEncryptionTaskResult.class, this);
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorChangeCacheGroupKeyTask.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorChangeCacheGroupKeyTask.java
new file mode 100644
index 0000000..d665941
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorChangeCacheGroupKeyTask.java
@@ -0,0 +1,61 @@
+/*
+ * 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.visor.encryption;
+
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.ignite.IgniteEncryption;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.internal.visor.VisorJob;
+import org.apache.ignite.internal.visor.VisorOneNodeTask;
+
+/**
+ * The task for changing the encryption key of the cache group.
+ *
+ * @see IgniteEncryption#changeCacheGroupKey(Collection)
+ */
+public class VisorChangeCacheGroupKeyTask extends VisorOneNodeTask<VisorCacheGroupEncryptionTaskArg, Void> {
+    /** Serial version uid. */
+    private static final long serialVersionUID = 0L;
+
+    /** {@inheritDoc} */
+    @Override protected VisorJob<VisorCacheGroupEncryptionTaskArg, Void> job(VisorCacheGroupEncryptionTaskArg arg) {
+        return new VisorChangeCacheGroupKeyJob(arg, debug);
+    }
+
+    /** The job for changing the encryption key of the cache group. */
+    private static class VisorChangeCacheGroupKeyJob extends VisorJob<VisorCacheGroupEncryptionTaskArg, Void> {
+        /** Serial version uid. */
+        private static final long serialVersionUID = 0L;
+
+        /**
+         * @param arg Job argument.
+         * @param debug Flag indicating whether debug information should be printed into node log.
+         */
+        protected VisorChangeCacheGroupKeyJob(VisorCacheGroupEncryptionTaskArg arg, boolean debug) {
+            super(arg, debug);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected Void run(VisorCacheGroupEncryptionTaskArg taskArg) throws IgniteException {
+            ignite.encryption().changeCacheGroupKey(Collections.singleton(taskArg.groupName())).get();
+
+            return null;
+        }
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorEncryptionKeyIdsTask.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorEncryptionKeyIdsTask.java
new file mode 100644
index 0000000..ca5a254
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorEncryptionKeyIdsTask.java
@@ -0,0 +1,83 @@
+/*
+ * 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.visor.encryption;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.List;
+import org.apache.ignite.internal.processors.cache.CacheGroupContext;
+import org.apache.ignite.internal.processors.task.GridInternal;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.internal.visor.VisorJob;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Get current encryption key IDs of the cache group.
+ */
+@GridInternal
+public class VisorEncryptionKeyIdsTask extends VisorCacheGroupEncryptionTask<List<Integer>> {
+    /** Serial version uid. */
+    private static final long serialVersionUID = 0L;
+
+    /** {@inheritDoc} */
+    @Override protected VisorJob<VisorCacheGroupEncryptionTaskArg, VisorSingleFieldDto<List<Integer>>> job(
+        VisorCacheGroupEncryptionTaskArg arg) {
+        return new VisorEncryptionKeyIdsJob(arg, debug);
+    }
+
+    /** The job for get current encryption key IDs of the cache group. */
+    private static class VisorEncryptionKeyIdsJob extends VisorReencryptionBaseJob<List<Integer>> {
+        /** Serial version uid. */
+        private static final long serialVersionUID = 0L;
+
+        /**
+         * @param arg Job argument.
+         * @param debug Flag indicating whether debug information should be printed into node log.
+         */
+        protected VisorEncryptionKeyIdsJob(@Nullable VisorCacheGroupEncryptionTaskArg arg, boolean debug) {
+            super(arg, debug);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected VisorSingleFieldDto<List<Integer>> run0(CacheGroupContext grp) {
+            return new VisorEncryptionKeyIdsResult().value(ignite.context().encryption().groupKeyIds(grp.groupId()));
+        }
+    }
+
+    /** */
+    protected static class VisorEncryptionKeyIdsResult extends VisorSingleFieldDto<List<Integer>> {
+        /** Serial version uid. */
+        private static final long serialVersionUID = 0L;
+
+        /** */
+        public VisorEncryptionKeyIdsResult() {
+            // No-op.
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void writeExternalData(ObjectOutput out) throws IOException {
+            U.writeCollection(out, value());
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void readExternalData(byte ver, ObjectInput in) throws IOException, ClassNotFoundException {
+            value(U.readList(in));
+        }
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorReencryptionRateTask.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorReencryptionRateTask.java
new file mode 100644
index 0000000..425fab5
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorReencryptionRateTask.java
@@ -0,0 +1,133 @@
+/*
+ * 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.visor.encryption;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.compute.ComputeJobResult;
+import org.apache.ignite.internal.dto.IgniteDataTransferObject;
+import org.apache.ignite.internal.processors.task.GridInternal;
+import org.apache.ignite.internal.visor.VisorJob;
+import org.apache.ignite.internal.visor.VisorMultiNodeTask;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * View/change cache group re-encryption rate limit .
+ */
+@GridInternal
+public class VisorReencryptionRateTask extends VisorMultiNodeTask<VisorReencryptionRateTaskArg,
+    VisorCacheGroupEncryptionTaskResult<Double>, VisorReencryptionRateTask.ReencryptionRateJobResult>
+{
+    /** Serial version uid. */
+    private static final long serialVersionUID = 0L;
+
+    /** {@inheritDoc} */
+    @Override protected VisorJob<VisorReencryptionRateTaskArg, ReencryptionRateJobResult> job(
+        VisorReencryptionRateTaskArg arg) {
+        return new VisorReencryptionRateJob(arg, debug);
+    }
+
+    /** {@inheritDoc} */
+    @Nullable @Override protected VisorCacheGroupEncryptionTaskResult<Double> reduce0(List<ComputeJobResult> results) {
+        Map<UUID, Double> jobResults = new HashMap<>();
+        Map<UUID, IgniteException> exceptions = new HashMap<>();
+
+        for (ComputeJobResult res : results) {
+            UUID nodeId = res.getNode().id();
+
+            if (res.getException() != null) {
+                exceptions.put(nodeId, res.getException());
+
+                continue;
+            }
+
+            ReencryptionRateJobResult dtoRes = res.getData();
+
+            jobResults.put(nodeId, dtoRes.limit());
+        }
+
+        return new VisorCacheGroupEncryptionTaskResult<>(jobResults, exceptions);
+    }
+
+    /** The job for view/change cache group re-encryption rate limit. */
+    private static class VisorReencryptionRateJob
+        extends VisorJob<VisorReencryptionRateTaskArg, ReencryptionRateJobResult> {
+        /** Serial version uid. */
+        private static final long serialVersionUID = 0L;
+
+        /**
+         * @param arg Job argument.
+         * @param debug Flag indicating whether debug information should be printed into node log.
+         */
+        protected VisorReencryptionRateJob(VisorReencryptionRateTaskArg arg, boolean debug) {
+            super(arg, debug);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected ReencryptionRateJobResult run(VisorReencryptionRateTaskArg arg) throws IgniteException {
+            double prevRate = ignite.context().encryption().getReencryptionRate();
+
+            if (arg.rate() != null)
+                ignite.context().encryption().setReencryptionRate(arg.rate());
+
+            return new ReencryptionRateJobResult(prevRate);
+        }
+    }
+
+    /** */
+    protected static class ReencryptionRateJobResult extends IgniteDataTransferObject {
+        /** Serial version uid. */
+        private static final long serialVersionUID = 0L;
+
+        /** Re-encryption rate limit. */
+        private Double limit;
+
+        /** */
+        public ReencryptionRateJobResult() {
+            // No-op.
+        }
+
+        /** */
+        public ReencryptionRateJobResult(Double limit) {
+            this.limit = limit;
+        }
+
+        /**
+         * @return Re-encryption rate limit.
+         */
+        public Double limit() {
+            return limit;
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void writeExternalData(ObjectOutput out) throws IOException {
+            out.writeDouble(limit);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void readExternalData(byte ver, ObjectInput in) throws IOException, ClassNotFoundException {
+            limit = in.readDouble();
+        }
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorReencryptionRateTaskArg.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorReencryptionRateTaskArg.java
new file mode 100644
index 0000000..2471296
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorReencryptionRateTaskArg.java
@@ -0,0 +1,74 @@
+/*
+ * 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.visor.encryption;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import org.apache.ignite.internal.dto.IgniteDataTransferObject;
+import org.apache.ignite.internal.util.typedef.internal.S;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Re-encryption rate task argument.
+ */
+public class VisorReencryptionRateTaskArg extends IgniteDataTransferObject {
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** Re-encryption rate limit in megabytes per second. */
+    private Double rate;
+
+    /** Default constructor. */
+    public VisorReencryptionRateTaskArg() {
+        // No-op.
+    }
+
+    /**
+     * @param rate Re-encryption rate limit in megabytes per second.
+     */
+    public VisorReencryptionRateTaskArg(Double rate) {
+        this.rate = rate;
+    }
+
+    /**
+     * @return Re-encryption rate limit in megabytes per second.
+     */
+    public @Nullable Double rate() {
+        return rate;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void writeExternalData(ObjectOutput out) throws IOException {
+        out.writeBoolean(rate != null);
+
+        if (rate != null)
+            out.writeDouble(rate);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void readExternalData(byte ver, ObjectInput in) throws IOException, ClassNotFoundException {
+        if (in.readBoolean())
+            rate = in.readDouble();
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(VisorReencryptionRateTaskArg.class, this);
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorReencryptionResumeTask.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorReencryptionResumeTask.java
new file mode 100644
index 0000000..171130c
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorReencryptionResumeTask.java
@@ -0,0 +1,60 @@
+/*
+ * 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.visor.encryption;
+
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.internal.processors.cache.CacheGroupContext;
+import org.apache.ignite.internal.processors.task.GridInternal;
+import org.apache.ignite.internal.visor.VisorJob;
+import org.apache.ignite.internal.visor.encryption.VisorReencryptionSuspendTask.VisorReencryptionSuspendResumeJobResult;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Resume re-encryption of the cache group.
+ */
+@GridInternal
+public class VisorReencryptionResumeTask extends VisorCacheGroupEncryptionTask<Boolean> {
+    /** Serial version uid. */
+    private static final long serialVersionUID = 0L;
+
+    /** {@inheritDoc} */
+    @Override protected VisorJob<VisorCacheGroupEncryptionTaskArg, VisorSingleFieldDto<Boolean>> job(
+        VisorCacheGroupEncryptionTaskArg arg) {
+        return new VisorReencryptionResumeJob(arg, debug);
+    }
+
+    /** The job to resume re-encryption of the cache group. */
+    private static class VisorReencryptionResumeJob extends VisorReencryptionBaseJob<Boolean> {
+        /** Serial version uid. */
+        private static final long serialVersionUID = 0L;
+
+        /**
+         * @param arg Job argument.
+         * @param debug Flag indicating whether debug information should be printed into node log.
+         */
+        protected VisorReencryptionResumeJob(@Nullable VisorCacheGroupEncryptionTaskArg arg, boolean debug) {
+            super(arg, debug);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected VisorSingleFieldDto<Boolean> run0(CacheGroupContext grp) throws IgniteCheckedException {
+            return new VisorReencryptionSuspendResumeJobResult().value(
+                ignite.context().encryption().resumeReencryption(grp.groupId()));
+        }
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorReencryptionStatusTask.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorReencryptionStatusTask.java
new file mode 100644
index 0000000..df60047
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorReencryptionStatusTask.java
@@ -0,0 +1,88 @@
+/*
+ * 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.visor.encryption;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import org.apache.ignite.internal.processors.cache.CacheGroupContext;
+import org.apache.ignite.internal.processors.task.GridInternal;
+import org.apache.ignite.internal.visor.VisorJob;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Get re-encryption status of the cache group.
+ */
+@GridInternal
+public class VisorReencryptionStatusTask extends VisorCacheGroupEncryptionTask<Long> {
+    /** Serial version uid. */
+    private static final long serialVersionUID = 0L;
+
+    /** {@inheritDoc} */
+    @Override protected VisorJob<VisorCacheGroupEncryptionTaskArg, VisorSingleFieldDto<Long>> job(
+        VisorCacheGroupEncryptionTaskArg arg) {
+        return new VisorReencryptionStatusJob(arg, debug);
+    }
+
+    /** The job to get re-encryption status of the cache group. */
+    private static class VisorReencryptionStatusJob extends VisorReencryptionBaseJob<Long> {
+        /** Serial version uid. */
+        private static final long serialVersionUID = 0L;
+
+        /**
+         * @param arg Job argument.
+         * @param debug Flag indicating whether debug information should be printed into node log.
+         */
+        protected VisorReencryptionStatusJob(@Nullable VisorCacheGroupEncryptionTaskArg arg, boolean debug) {
+            super(arg, debug);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected VisorSingleFieldDto<Long> run0(CacheGroupContext grp) {
+            long res;
+
+            if (!ignite.context().encryption().reencryptionInProgress(grp.groupId()))
+                res = -1;
+            else
+                res = ignite.context().encryption().getBytesLeftForReencryption(grp.groupId());
+
+            return new VisorReencryptionStatusResult().value(res);
+        }
+    }
+
+    /** */
+    protected static class VisorReencryptionStatusResult extends VisorSingleFieldDto<Long> {
+        /** Serial version uid. */
+        private static final long serialVersionUID = 0L;
+
+        /** */
+        public VisorReencryptionStatusResult() {
+            // No-op.
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void writeExternalData(ObjectOutput out) throws IOException {
+            out.writeLong(value());
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void readExternalData(byte ver, ObjectInput in) throws IOException, ClassNotFoundException {
+            value(in.readLong());
+        }
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorReencryptionSuspendTask.java b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorReencryptionSuspendTask.java
new file mode 100644
index 0000000..edbfd46
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/visor/encryption/VisorReencryptionSuspendTask.java
@@ -0,0 +1,83 @@
+/*
+ * 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.visor.encryption;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.internal.processors.cache.CacheGroupContext;
+import org.apache.ignite.internal.processors.task.GridInternal;
+import org.apache.ignite.internal.visor.VisorJob;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Suspend re-encryption of the cache group.
+ */
+@GridInternal
+public class VisorReencryptionSuspendTask extends VisorCacheGroupEncryptionTask<Boolean> {
+    /** Serial version uid. */
+    private static final long serialVersionUID = 0L;
+
+    /** {@inheritDoc} */
+    @Override protected VisorJob<VisorCacheGroupEncryptionTaskArg, VisorSingleFieldDto<Boolean>> job(
+        VisorCacheGroupEncryptionTaskArg arg) {
+        return new VisorReencryptionSuspendJob(arg, debug);
+    }
+
+    /** The job to suspend re-encryption of the cache group. */
+    private static class VisorReencryptionSuspendJob extends VisorReencryptionBaseJob<Boolean> {
+        /** Serial version uid. */
+        private static final long serialVersionUID = 0L;
+
+        /**
+         * @param arg Job argument.
+         * @param debug Flag indicating whether debug information should be printed into node log.
+         */
+        protected VisorReencryptionSuspendJob(@Nullable VisorCacheGroupEncryptionTaskArg arg, boolean debug) {
+            super(arg, debug);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected VisorSingleFieldDto<Boolean> run0(CacheGroupContext grp) throws IgniteCheckedException {
+            return new VisorReencryptionSuspendResumeJobResult().value(
+                ignite.context().encryption().suspendReencryption(grp.groupId()));
+        }
+    }
+
+    /** */
+    protected static class VisorReencryptionSuspendResumeJobResult extends VisorSingleFieldDto<Boolean> {
+        /** Serial version uid. */
+        private static final long serialVersionUID = 0L;
+
+        /** */
+        public VisorReencryptionSuspendResumeJobResult() {
+            // No-op.
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void writeExternalData(ObjectOutput out) throws IOException {
+            out.writeBoolean(value());
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void readExternalData(byte ver, ObjectInput in) throws IOException, ClassNotFoundException {
+            value(in.readBoolean());
+        }
+    }
+}
diff --git a/modules/core/src/main/resources/META-INF/classnames.properties b/modules/core/src/main/resources/META-INF/classnames.properties
index f480d0f..1548b78 100644
--- a/modules/core/src/main/resources/META-INF/classnames.properties
+++ b/modules/core/src/main/resources/META-INF/classnames.properties
@@ -2314,6 +2314,25 @@ org.apache.ignite.internal.visor.encryption.VisorGetMasterKeyNameTask
 org.apache.ignite.internal.visor.encryption.VisorGetMasterKeyNameTask$VisorGetMasterKeyNameJob
 org.apache.ignite.internal.visor.encryption.VisorChangeMasterKeyTask
 org.apache.ignite.internal.visor.encryption.VisorChangeMasterKeyTask$VisorChangeMasterKeyJob
+org.apache.ignite.internal.visor.encryption.VisorCacheGroupEncryptionTaskArg
+org.apache.ignite.internal.visor.encryption.VisorCacheGroupEncryptionTaskResult
+org.apache.ignite.internal.visor.encryption.VisorChangeCacheGroupKeyTask
+org.apache.ignite.internal.visor.encryption.VisorChangeCacheGroupKeyTask$VisorChangeCacheGroupKeyJob
+org.apache.ignite.internal.visor.encryption.VisorEncryptionKeyIdsTask
+org.apache.ignite.internal.visor.encryption.VisorEncryptionKeyIdsTask$VisorEncryptionKeyIdsJob
+org.apache.ignite.internal.visor.encryption.VisorEncryptionKeyIdsTask$VisorEncryptionKeyIdsResult
+org.apache.ignite.internal.visor.encryption.VisorReencryptionRateTask
+org.apache.ignite.internal.visor.encryption.VisorReencryptionRateTask$VisorReencryptionRateJob
+org.apache.ignite.internal.visor.encryption.VisorReencryptionRateTask$ReencryptionRateJobResult
+org.apache.ignite.internal.visor.encryption.VisorReencryptionRateTaskArg
+org.apache.ignite.internal.visor.encryption.VisorReencryptionResumeTask
+org.apache.ignite.internal.visor.encryption.VisorReencryptionResumeTask$VisorReencryptionResumeJob
+org.apache.ignite.internal.visor.encryption.VisorReencryptionStatusTask
+org.apache.ignite.internal.visor.encryption.VisorReencryptionStatusTask$VisorReencryptionStatusJob
+org.apache.ignite.internal.visor.encryption.VisorReencryptionStatusTask$VisorReencryptionStatusResult
+org.apache.ignite.internal.visor.encryption.VisorReencryptionSuspendTask
+org.apache.ignite.internal.visor.encryption.VisorReencryptionSuspendTask$VisorReencryptionSuspendJob
+org.apache.ignite.internal.visor.encryption.VisorReencryptionSuspendTask$VisorReencryptionSuspendResumeJobResult
 org.apache.ignite.internal.visor.util.VisorClusterGroupEmptyException
 org.apache.ignite.internal.visor.util.VisorEventMapper
 org.apache.ignite.internal.visor.util.VisorExceptionWrapper
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/encryption/CacheGroupReencryptionTest.java b/modules/core/src/test/java/org/apache/ignite/internal/encryption/CacheGroupReencryptionTest.java
index 1f7cf36..19c8351 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/encryption/CacheGroupReencryptionTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/encryption/CacheGroupReencryptionTest.java
@@ -385,7 +385,7 @@ public class CacheGroupReencryptionTest extends AbstractEncryptionTest {
     @Test
     public void testPartitionFileDestroy() throws Exception {
         backups = 1;
-        pageScanRate = 1;
+        pageScanRate = 0.2;
         pageScanBatchSize = 10;
 
         T2<IgniteEx, IgniteEx> nodes = startTestGrids(true);
@@ -409,6 +409,10 @@ public class CacheGroupReencryptionTest extends AbstractEncryptionTest {
 
         assertTrue(isReencryptionInProgress(Collections.singleton(cacheName())));
 
+        // Set unlimited re-encryption rate.
+        nodes.get1().context().encryption().setReencryptionRate(0);
+        nodes.get2().context().encryption().setReencryptionRate(0);
+
         checkGroupKey(CU.cacheId(cacheName()), INITIAL_KEY_ID + 1, MAX_AWAIT_MILLIS);
     }
 
@@ -758,12 +762,12 @@ public class CacheGroupReencryptionTest extends AbstractEncryptionTest {
         MetricRegistry registry =
             node.context().metric().registry(metricName(CacheGroupMetricsImpl.CACHE_GROUP_METRICS_PREFIX, cacheName()));
 
-        LongMetric pagesLeft = registry.findMetric("ReencryptionPagesLeft");
+        LongMetric bytesLeft = registry.findMetric("ReencryptionBytesLeft");
 
         if (finished)
-            assertEquals(0, pagesLeft.value());
+            assertEquals(0, bytesLeft.value());
         else
-            assertTrue(pagesLeft.value() > 0);
+            assertTrue(bytesLeft.value() > 0);
 
         BooleanMetric reencryptionFinished = registry.findMetric("ReencryptionFinished");
 
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/BasicRateLimiterTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/BasicRateLimiterTest.java
index c136cb8..9f4bca9 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/util/BasicRateLimiterTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/util/BasicRateLimiterTest.java
@@ -68,6 +68,21 @@ public class BasicRateLimiterTest {
     }
 
     /**
+     * Check that the rate can be set as unlimited.
+     */
+    @Test
+    public void testUnlimitedRate() throws IgniteInterruptedCheckedException {
+        BasicRateLimiter limiter = new BasicRateLimiter(0);
+        limiter.acquire(Integer.MAX_VALUE);
+
+        limiter.setRate(1);
+        limiter.acquire(1);
+
+        limiter.setRate(0);
+        limiter.acquire(Integer.MAX_VALUE);
+    }
+
+    /**
      * Check rate limit with multiple threads.
      */
     @Test
diff --git a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_help.output b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_help.output
index a4c2ec4..ef98769 100644
--- a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_help.output
+++ b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassTest_help.output
@@ -71,6 +71,27 @@ This utility can do the following commands:
   Change the master key:
     control.(sh|bat) --encryption change_master_key newMasterKeyName
 
+  Change the encryption key of the cache group:
+    control.(sh|bat) --encryption change_cache_key cacheGroupName
+
+  View encryption key identifiers of the cache group:
+    control.(sh|bat) --encryption cache_key_ids cacheGroupName
+
+  Display re-encryption status of the cache group:
+    control.(sh|bat) --encryption reencryption_status cacheGroupName
+
+  Suspend re-encryption of the cache group:
+    control.(sh|bat) --encryption suspend_reencryption cacheGroupName
+
+  Resume re-encryption of the cache group:
+    control.(sh|bat) --encryption resume_reencryption cacheGroupName
+
+  View/change re-encryption rate limit:
+    control.(sh|bat) --encryption reencryption_rate_limit [new_limit]
+
+    Parameters:
+      new_limit  - Decimal value to change re-encryption rate limit (MB/s).
+
   Kill compute task by session id:
     control.(sh|bat) --kill COMPUTE session_id
 
diff --git a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_help.output b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_help.output
index a4c2ec4..ef98769 100644
--- a/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_help.output
+++ b/modules/core/src/test/resources/org.apache.ignite.util/GridCommandHandlerClusterByClassWithSSLTest_help.output
@@ -71,6 +71,27 @@ This utility can do the following commands:
   Change the master key:
     control.(sh|bat) --encryption change_master_key newMasterKeyName
 
+  Change the encryption key of the cache group:
+    control.(sh|bat) --encryption change_cache_key cacheGroupName
+
+  View encryption key identifiers of the cache group:
+    control.(sh|bat) --encryption cache_key_ids cacheGroupName
+
+  Display re-encryption status of the cache group:
+    control.(sh|bat) --encryption reencryption_status cacheGroupName
+
+  Suspend re-encryption of the cache group:
+    control.(sh|bat) --encryption suspend_reencryption cacheGroupName
+
+  Resume re-encryption of the cache group:
+    control.(sh|bat) --encryption resume_reencryption cacheGroupName
+
+  View/change re-encryption rate limit:
+    control.(sh|bat) --encryption reencryption_rate_limit [new_limit]
+
+    Parameters:
+      new_limit  - Decimal value to change re-encryption rate limit (MB/s).
+
   Kill compute task by session id:
     control.(sh|bat) --kill COMPUTE session_id