You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hugegraph.apache.org by ji...@apache.org on 2026/10/17 20:42:06 UTC

(hugegraph) branch master updated: refactor: improve activation and local diagnosis tool (#2945)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new d505f6f02 refactor: improve activation and local diagnosis tool (#2945)
d505f6f02 is described below

commit d505f6f02503d36ff4f6735686cede3500640a51
Author: Soyaazz <52...@qq.com>
AuthorDate: Sun Oct 18 04:42:01 2026 +0800

    refactor: improve activation and local diagnosis tool (#2945)
    
    Arthas could previously be activated through unprotected HTTP endpoints, with diagnostic ports bound to 0.0.0.0. Server now requires configured authentication and an authenticated administrator for PUT /arthas; Store's unauthenticated /v1/arthasstart returns HTTP 403.
---
 .github/workflows/pd-store-ci.yml                  |  12 +-
 .github/workflows/server-tests.yml                 |   7 +
 docs/server-runtime.md                             | 137 ++++++-
 hugegraph-server/Dockerfile                        |   3 +-
 hugegraph-server/Dockerfile-hstore                 |   3 +-
 .../org/apache/hugegraph/api/arthas/ArthasAPI.java |  75 +++-
 .../org/apache/hugegraph/config/ServerOptions.java |  38 +-
 hugegraph-server/hugegraph-dist/pom.xml            |  27 ++
 .../src/assembly/descriptor/assembly.xml           |   4 +
 .../src/assembly/static/bin/ArthasAttach.java      |  96 +++++
 .../src/assembly/static/bin/arthas.sh              | 232 ++++++++++++
 .../src/assembly/static/bin/gremlin-console.sh     |   1 +
 .../src/assembly/static/bin/hugegraph-server.sh    |   1 +
 .../src/assembly/static/bin/init-store.sh          |   3 +-
 .../assembly/static/conf/rest-server.properties    |  12 +-
 .../src/assembly/travis/test-arthas-cli.sh         | 404 +++++++++++++++++++++
 .../assembly/travis/test-check-jacoco-report.sh    |   2 +
 .../org/apache/hugegraph/api/ArthasApiTest.java    |  33 +-
 .../hugegraph/api/arthas/ArthasConfigTest.java     | 244 +++++++++++++
 .../org/apache/hugegraph/unit/UnitTestSuite.java   |   2 +
 hugegraph-server/pom.xml                           |   1 -
 hugegraph-store/Dockerfile                         |   3 +-
 hugegraph-store/README.md                          |   3 +
 hugegraph-store/hg-store-dist/pom.xml              |  33 ++
 .../src/assembly/descriptor/server-assembly.xml    |  15 +-
 .../src/assembly/static/conf/application.yml       |   3 +
 hugegraph-store/hg-store-node/pom.xml              |  11 -
 .../org/apache/hugegraph/store/node/AppConfig.java |   4 +-
 .../store/node/controller/PartitionAPI.java        |  26 +-
 .../src/main/resources/application.yml             |   3 +
 .../store/node/controller/ArthasSecurityTest.java  |  85 +++++
 install-dist/release-docs/LICENSE                  |   6 +-
 .../scripts/dependency/known-dependencies.txt      |   9 +-
 pom.xml                                            |   2 +
 34 files changed, 1471 insertions(+), 69 deletions(-)

diff --git a/.github/workflows/pd-store-ci.yml b/.github/workflows/pd-store-ci.yml
index 8dcc74b8f..ceb1d1121 100644
--- a/.github/workflows/pd-store-ci.yml
+++ b/.github/workflows/pd-store-ci.yml
@@ -374,6 +374,12 @@ jobs:
         run: |
           mvn clean package -U -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -ntp --fail-at-end
 
+      - name: Verify Store Arthas local CLI attachment
+        run: |
+          VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
+          STORE_DIR=hugegraph-store/apache-hugegraph-store-$VERSION
+          bash "$TRAVIS_DIR/test-arthas-cli.sh" "$STORE_DIR"
+
       - name: Verify Surefire single-class routing
         run: |
           SUREFIRE_DIR=hugegraph-store/hg-store-test/target/surefire-reports
@@ -446,12 +452,12 @@ jobs:
           echo "PD_DIR=hugegraph-pd/apache-hugegraph-pd-$VERSION" >> "$GITHUB_ENV"
           echo "STORE_DIR=hugegraph-store/apache-hugegraph-store-$VERSION" >> "$GITHUB_ENV"
 
-      - name: Run historical Store ingress tests
+      - name: Run Store ingress and Arthas security tests
         run: |
           set -o pipefail
           RUNTIME_LOG=$(mktemp "$RUNNER_TEMP/java-runtime.XXXXXX")
           mvn test -pl hugegraph-store/hg-store-node -am \
-            -Dtest=StoredRowIngressTest -Dsurefire.failIfNoSpecifiedTests=false \
+            -Dtest=StoredRowIngressTest,ArthasSecurityTest -Dsurefire.failIfNoSpecifiedTests=false \
             -Djacoco.sessionId=store-node-test \
             -Djacoco.destFile="$PWD/hugegraph-store/hg-store-test/target/store-node-test.exec" 2>&1 | tee "$RUNTIME_LOG"
           bash "$TRAVIS_DIR/check-java-runtime-logs.sh" "$RUNTIME_LOG" "$PD_DIR/logs" "$STORE_DIR/logs"
@@ -526,6 +532,8 @@ jobs:
             "$TEST_REPORT_DIR/TEST-org.apache.hugegraph.store.service.ServerSuiteTest.xml" \
             --require-test-report \
             "hugegraph-store/hg-store-node/target/surefire-reports/TEST-org.apache.hugegraph.store.business.StoredRowIngressTest.xml" \
+            --require-test-report \
+            "hugegraph-store/hg-store-node/target/surefire-reports/TEST-org.apache.hugegraph.store.node.controller.ArthasSecurityTest.xml" \
             --require-covered-group hg-store-common \
             --require-covered-group hg-store-client \
             --require-covered-group hg-store-rocksdb \
diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml
index c503bacf9..1f707604e 100644
--- a/.github/workflows/server-tests.yml
+++ b/.github/workflows/server-tests.yml
@@ -104,6 +104,13 @@ jobs:
           $TRAVIS_DIR/test-java-security-properties.sh \
             "$SERVER_DIR" "$GITHUB_WORKSPACE"
 
+      - name: Check bundled Arthas local CLI
+        if: ${{ env.BACKEND == 'rocksdb' }}
+        run: |
+          VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
+          SERVER_DIR=hugegraph-server/apache-hugegraph-server-$VERSION
+          bash "$TRAVIS_DIR/test-arthas-cli.sh" "$SERVER_DIR"
+
       - name: Verify Surefire single-class routing
         if: ${{ env.BACKEND == 'rocksdb' }}
         run: |
diff --git a/docs/server-runtime.md b/docs/server-runtime.md
index d3466cce1..ec997c0cd 100644
--- a/docs/server-runtime.md
+++ b/docs/server-runtime.md
@@ -79,11 +79,138 @@ existing precedence; see [container heap sizing](../docker/README.md#server-heap
 
 ### Arthas diagnostics
 
-Server bundles Arthas 4.1.5. Activate the agent with `PUT /arthas` using the Server's
-configured authentication. The existing `arthas.ip`, `arthas.httpPort`,
-`arthas.telnetPort` and `arthas.disabledCommands` options still configure the agent.
-HugeGraph disables the bundled agent's optional MCP endpoint and keeps the existing
-activation response and diagnostic HTTP/telnet interfaces.
+Server and Store bundle Arthas 4.1.5, managed by the root POM. PD does not embed
+an Arthas agent. Diagnostic ports bind to `127.0.0.1` by default. Administrators
+can explicitly configure another valid address, including wildcard `0.0.0.0` or
+`::`; `localhost` is normalized to `127.0.0.1`. Wildcard listeners can accept both
+IPv4 and IPv6 depending on the OS/JVM configuration; restrict both in firewall
+rules. Server's `PUT /arthas`
+requires authentication to be enabled and the caller to be an authenticated administrator.
+Without authentication, the Server endpoint returns HTTP 403, including requests
+from localhost or through a reverse proxy. Store has no equivalent administrator
+authentication, so `GET /v1/arthasstart` always returns HTTP 403.
+
+For local diagnosis of either running service, use its distribution's bundled
+helper as the same OS user that owns the JVM:
+
+```bash
+bin/arthas.sh
+```
+
+The distribution prepares the agent in `lib/arthas` at build time. The helper
+requires JDK 17 and uses `bin/pid`, without downloads, runtime extraction, a user
+cache, service restarts or configuration edits. A new local agent listens on
+`127.0.0.1:3658`, with HTTP, MCP and reverse tunnels disabled. Use `--pid` or
+`--telnet-port` for another running instance, and `--command version` for a one-time
+command. Interactive mode requires a terminal; for pipes, scripts or `docker exec`
+without `-it`, use `--command` instead. Run `bin/arthas.sh --help` for usage.
+When the helper exits, including after `--command`, `quit`, or INT/TERM/HUP, it
+stops the agent and all its diagnostic sessions; the application JVM keeps running.
+SIGKILL or a host failure cannot run this cleanup: stop any surviving agent manually.
+For remote diagnosis, log in to the service host over SSH and run the same command,
+or explicitly configure a reachable listener with appropriate authentication and
+network access controls as described below.
+Official Server (including HStore) and Store images include JDK 17, so the same
+helper works inside the container, for example
+`docker exec -it <store-container> /hugegraph-store/bin/arthas.sh`.
+A JRE-only installation needs JDK 17 for this helper.
+If the Server API has already activated Arthas, connect to its existing telnet
+port instead: `bin/arthas.sh --telnet-port 8562` with the default configuration.
+Use `--target-ip` to select another bind address, for example
+`bin/arthas.sh --target-ip 0.0.0.0` or
+`bin/arthas.sh --target-ip ::1 --telnet-port 8562 --command version`.
+The default is `127.0.0.1`; `localhost` maps to `127.0.0.1`. Wildcard addresses
+`0.0.0.0` and `::` may accept both IPv4 and IPv6; the helper connects and cleans
+up through `127.0.0.1` for either wildcard. Remote clients connect
+to the service host's actual reachable address, not the wildcard address.
+An already running agent retains its listener and authentication settings;
+helper options do not reconfigure it. To switch to the helper's
+HTTP-disabled defaults, run `bin/arthas.sh --telnet-port 8562 --command stop`, then
+`bin/arthas.sh`; this stops only Arthas, and leaves the service JVM running.
+Arthas can execute code inside the JVM. OS attach permissions control initial
+activation; HugeGraph administrator authentication protects only the activation
+API, not subsequent diagnostic connections. We recommend enabling Arthas native
+authentication, especially when binding beyond loopback. However, native defaults
+allow connections from `127.0.0.1` to skip authentication even with credentials;
+other OS users or containers in the same Pod can still connect this way. To disable
+this bypass, set `-Darthas.localConnectionNonAuth=false` on the service JVM and
+restart it. Restrict host/Pod and port-forward access to trusted operators, and
+limit firewall rules, port mappings and SSH forwarding accordingly. Authentication
+does not encrypt telnet
+or ordinary HTTP traffic; use a protected network or encrypted transport for
+remote access. Avoid concurrent helper sessions because exiting one stops the
+shared agent. A disabled-command list is optional and is not access control.
+
+Pass native Arthas credentials when starting or reusing an agent:
+
+```bash
+bin/arthas.sh --target-ip 0.0.0.0 \
+    --username diagnostics --password '<arthas-password>'
+```
+
+Use single-line credentials; printable ASCII avoids the bundled Arthas 4.1.5
+telnet client's Unicode limitation. Password-only use defaults to username `arthas`.
+Native JVM credentials also apply to helper-started agents: if the target JVM has
+`-Darthas.username` or `-Darthas.password`, supply the effective credentials with
+`--username` / `--password` for authenticated commands and cleanup, especially
+when `-Darthas.localConnectionNonAuth=false` is set. The helper does not clear
+native credentials when the corresponding option is omitted.
+These credentials are separate from HugeGraph accounts. Batch commands and exit
+cleanup use the supplied credentials automatically. For interactive diagnosis,
+if prompted, enter `auth --username diagnostics <arthas-password>` in the Arthas
+console. Existing agents require their existing credentials; passing new values
+does not change them. Password arguments may appear in shell history and process
+listings, so handle them on a trusted host. With `--username` alone, native Arthas
+generates a password only when none is already configured in the target JVM's
+native settings; otherwise it uses that inherited password. Supply `--password` for
+authenticated batch commands and automatic cleanup. If cleanup fails, the helper
+returns nonzero and the agent may remain running; use the configured or generated credentials with
+native `auth`, then `stop`, to close it without stopping the service JVM.
+
+[Arthas native authentication](https://arthas.aliyun.com/en/doc/auth.html) remains
+optional and follows upstream defaults: `arthas.localConnectionNonAuth=true`
+allows connections from `127.0.0.1` to skip authentication. The helper uses a private
+configuration, so editing `lib/arthas/arthas.properties` does not configure it.
+If required, the target JVM property `-Darthas.localConnectionNonAuth=false` is
+honored after restarting the service JVM. Arthas 4.1.5 generates a password when
+binding `0.0.0.0` or `::` without an effective native password and prints it in the target
+JVM output and Arthas log; explicitly supplying credentials is more convenient
+for remote diagnosis and authenticated cleanup.
+
+Authenticated Server administrators can continue using `PUT /arthas` without an
+additional enable switch or restart. Its HTTP/telnet interfaces use the configured
+bind address; HugeGraph disables the optional MCP endpoint. The activation response
+still contains the same four Arthas configuration keys. For API activation with
+native credentials, add `-Darthas.username=diagnostics` and
+`-Darthas.password=<arthas-password>` to the Server JVM startup options and restart
+Server before activation. These are native JVM properties, not new REST
+configuration keys, and also apply to agents started by the local helper as described
+above. They may expose secrets in process listings. Without native
+credential settings, a specific non-loopback bind address does not require login; wildcard
+binds use the generated password described above. API activation alone
+leaves the agent running until an operator sends `stop`; connecting through the
+helper applies the helper's exit cleanup to that existing agent too.
+
+Before reusing an API-started agent, leave `session` and `stop` out of
+`arthas.disabled_commands` (or its legacy alias). `session` verifies the target
+PID; `stop` closes Arthas without stopping the service JVM. Existing configurations
+remain accepted, but failed PID verification or listener cleanup makes the helper
+print an error and exit nonzero; the agent may remain running. To recover from an
+agent with `stop` disabled, remove that restriction and restart the service before
+activating Arthas again. Newly attached local agents keep both commands available.
+
+No commands are disabled by default. Administrators can optionally set
+`arthas.disabled_commands=jad,mc` to restrict selected commands for API activation;
+new local agents do not inherit this list.
+
+The existing `arthas.ip`, `arthas.httpPort`, `arthas.telnetPort` and
+`arthas.disabledCommands` options configure the Server API-activated agent.
+The distribution uses `arthas.telnet_port`, `arthas.http_port` and
+`arthas.disabled_commands`. These snake_case keys take precedence over the legacy
+camelCase aliases when both are present, including explicit default values.
+Conflicting values produce a warning naming both keys and the effective key when
+the agent is activated. Remove the unused alias to avoid ambiguity. Store's legacy
+configuration fields are retained; the local helper does not read them.
 
 For a synchronous one-time command, POST `{"action":"exec","command":"version"}`
 to the agent's `/api` endpoint without a `sessionId`. Check `state=SUCCEEDED`, the
diff --git a/hugegraph-server/Dockerfile b/hugegraph-server/Dockerfile
index 1ec3ce8bd..694acd6f2 100644
--- a/hugegraph-server/Dockerfile
+++ b/hugegraph-server/Dockerfile
@@ -35,7 +35,8 @@ RUN --mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sh
 
 # 2nd stage: runtime env
 # Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13
-FROM eclipse-temurin:17-jre-jammy
+FROM eclipse-temurin:17-jdk-jammy
+# Local Arthas attach requires the JDK attach and compiler modules.
 
 LABEL maintainer="HugeGraph Docker Maintainers <de...@hugegraph.apache.org>"
 
diff --git a/hugegraph-server/Dockerfile-hstore b/hugegraph-server/Dockerfile-hstore
index 1b2f0e531..351b116d6 100644
--- a/hugegraph-server/Dockerfile-hstore
+++ b/hugegraph-server/Dockerfile-hstore
@@ -35,7 +35,8 @@ RUN --mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sh
 
 # 2nd stage: runtime env
 # Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13
-FROM eclipse-temurin:17-jre-jammy
+FROM eclipse-temurin:17-jdk-jammy
+# Local Arthas attach requires the JDK attach and compiler modules.
 
 LABEL maintainer="HugeGraph Docker Maintainers <de...@hugegraph.apache.org>"
 
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/arthas/ArthasAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/arthas/ArthasAPI.java
index 77e961d4d..f2dd9e980 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/arthas/ArthasAPI.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/arthas/ArthasAPI.java
@@ -17,48 +17,109 @@
 
 package org.apache.hugegraph.api.arthas;
 
+import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.util.HashMap;
 
 import org.apache.hugegraph.api.API;
+import org.apache.hugegraph.auth.HugeAuthenticator;
+import org.apache.hugegraph.config.ConfigOption;
 import org.apache.hugegraph.config.HugeConfig;
 import org.apache.hugegraph.config.ServerOptions;
+import org.apache.hugegraph.core.GraphManager;
 import org.apache.hugegraph.util.JsonUtil;
+import org.apache.hugegraph.util.Log;
+import org.slf4j.Logger;
 
 import com.codahale.metrics.annotation.Timed;
 import com.taobao.arthas.agent.attach.ArthasAgent;
 
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.security.RolesAllowed;
 import jakarta.inject.Singleton;
+import jakarta.ws.rs.ForbiddenException;
 import jakarta.ws.rs.PUT;
 import jakarta.ws.rs.Path;
 import jakarta.ws.rs.Produces;
 import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.SecurityContext;
 
 @Path("arthas")
 @Singleton
 @Tag(name = "ArthasAPI")
 public class ArthasAPI extends API {
 
+    private static final Logger LOG = Log.logger(ArthasAPI.class);
+
     @Context
     private jakarta.inject.Provider<HugeConfig> configProvider;
 
     @PUT
     @Timed
     @Produces(APPLICATION_JSON_WITH_CHARSET)
+    @RolesAllowed("admin")
     @Operation(summary = "start arthas agent")
-    public Object startArthas() {
+    public Object startArthas(@Context GraphManager manager,
+                             @Context SecurityContext context) {
+        checkAccess(manager, context);
         HugeConfig config = this.configProvider.get();
-        HashMap<String, String> configMap = new HashMap<>(4);
-        configMap.put("arthas.telnetPort", config.get(ServerOptions.ARTHAS_TELNET_PORT));
-        configMap.put("arthas.httpPort", config.get(ServerOptions.ARTHAS_HTTP_PORT));
-        configMap.put("arthas.ip", config.get(ServerOptions.ARTHAS_IP));
-        configMap.put("arthas.disabledCommands",
-                      config.get(ServerOptions.ARTHAS_DISABLED_COMMANDS));
+        HashMap<String, String> configMap = arthasConfig(config);
         String response = JsonUtil.toJson(configMap);
         // Keep the optional MCP endpoint disabled without extending the activation response.
         configMap.put("arthas.mcpEndpoint", "");
+        // Do not let external Arthas properties override the configured listener.
+        configMap.put("arthas.config.overrideAll", "false");
+        configMap.put("arthas.tunnelServer", "");
         ArthasAgent.attach(configMap);
         return response;
     }
+
+    static void checkAccess(GraphManager manager, SecurityContext context) {
+        // No-auth requests receive an anonymous admin role, so a role check alone is insufficient.
+        if (!manager.requireAuthentication()) {
+            throw new ForbiddenException("HTTP Arthas activation requires authentication; " +
+                                         "use bin/arthas.sh on the server host instead");
+        }
+        if (context == null || context.getUserPrincipal() == null ||
+            HugeAuthenticator.USER_ANONY.equals(context.getUserPrincipal().getName()) ||
+            !context.isUserInRole("admin")) {
+            throw new ForbiddenException("Arthas activation requires an authenticated administrator");
+        }
+    }
+
+    static HashMap<String, String> arthasConfig(HugeConfig config) {
+        HashMap<String, String> configMap = new HashMap<>(4);
+        configMap.put("arthas.telnetPort",
+                      option(config, ServerOptions.ARTHAS_TELNET_PORT,
+                             ServerOptions.ARTHAS_TELNET_PORT_LEGACY));
+        configMap.put("arthas.httpPort",
+                      option(config, ServerOptions.ARTHAS_HTTP_PORT,
+                             ServerOptions.ARTHAS_HTTP_PORT_LEGACY));
+        String ip = config.get(ServerOptions.ARTHAS_IP);
+        ip = "localhost".equalsIgnoreCase(ip) ? "127.0.0.1" : ip;
+        try {
+            InetAddress.getByName(ip);
+        } catch (UnknownHostException e) {
+            throw new IllegalArgumentException("Invalid or unresolvable Arthas bind address '" + ip + "'", e);
+        }
+        configMap.put("arthas.ip", ip);
+        configMap.put("arthas.disabledCommands",
+                      option(config, ServerOptions.ARTHAS_DISABLED_COMMANDS,
+                             ServerOptions.ARTHAS_DISABLED_COMMANDS_LEGACY));
+        return configMap;
+    }
+
+    private static String option(HugeConfig config, ConfigOption<String> canonical,
+                                 ConfigOption<String> legacy) {
+        if (!config.containsKey(canonical.name())) {
+            return config.get(legacy);
+        }
+        String value = config.get(canonical);
+        if (config.containsKey(legacy.name()) && !value.equals(config.get(legacy))) {
+            LOG.warn("Conflicting Arthas options '{}' and '{}': '{}' takes precedence",
+                     canonical.name(), legacy.name(), canonical.name());
+        }
+        return value;
+    }
 }
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java
index 285c4b23a..3441291e4 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/config/ServerOptions.java
@@ -433,34 +433,58 @@ public class ServerOptions extends OptionHolder {
 
     public static final ConfigOption<String> ARTHAS_TELNET_PORT =
             new ConfigOption<>(
-                    "arthas.telnetPort",
+                    "arthas.telnet_port",
                     "arthas provides telnet ports to the outside",
                     disallowEmpty(),
                     "8562"
             );
 
+    public static final ConfigOption<String> ARTHAS_TELNET_PORT_LEGACY =
+            new ConfigOption<>(
+                    "arthas.telnetPort",
+                    "Legacy alias: arthas provides telnet ports to the outside",
+                    disallowEmpty(),
+                    "8562"
+            );
+
     public static final ConfigOption<String> ARTHAS_HTTP_PORT =
             new ConfigOption<>(
-                    "arthas.httpPort",
+                    "arthas.http_port",
                     "arthas provides http ports to the outside",
                     disallowEmpty(),
                     "8561"
             );
 
+    public static final ConfigOption<String> ARTHAS_HTTP_PORT_LEGACY =
+            new ConfigOption<>(
+                    "arthas.httpPort",
+                    "Legacy alias: arthas provides http ports to the outside",
+                    disallowEmpty(),
+                    "8561"
+            );
+
     public static final ConfigOption<String> ARTHAS_IP =
             new ConfigOption<>(
                     "arthas.ip",
-                    "arthas bound ip",
+                    "Arthas bind address; use loopback unless external access is intended",
                     disallowEmpty(),
-                    "0.0.0.0"
+                    "127.0.0.1"
             );
 
     public static final ConfigOption<String> ARTHAS_DISABLED_COMMANDS =
+            new ConfigOption<>(
+                    "arthas.disabled_commands",
+                    "Optional comma-separated Arthas disabled commands, empty by default",
+                    null,
+                    ""
+            );
+
+    public static final ConfigOption<String> ARTHAS_DISABLED_COMMANDS_LEGACY =
             new ConfigOption<>(
                     "arthas.disabledCommands",
-                    "arthas disabled commands",
-                    disallowEmpty(),
-                    "jad"
+                    "Legacy alias: optional comma-separated Arthas disabled commands",
+                    null,
+                    ""
             );
 
     public static final ConfigOption<Boolean> ALLOW_TRACE =
diff --git a/hugegraph-server/hugegraph-dist/pom.xml b/hugegraph-server/hugegraph-dist/pom.xml
index c4575bc87..810bd281b 100644
--- a/hugegraph-server/hugegraph-dist/pom.xml
+++ b/hugegraph-server/hugegraph-dist/pom.xml
@@ -183,6 +183,33 @@
                     <artifactId>maven-antrun-plugin</artifactId>
                     <version>1.8</version>
                     <executions>
+                        <execution>
+                            <id>prepare-arthas</id>
+                            <phase>prepare-package</phase>
+                            <goals>
+                                <goal>run</goal>
+                            </goals>
+                            <configuration>
+                                <target>
+                                    <unzip src="${com.taobao.arthas:arthas-packaging:jar}"
+                                           dest="${project.build.directory}/arthas-packaging">
+                                        <patternset>
+                                            <include name="arthas-bin.zip"/>
+                                        </patternset>
+                                    </unzip>
+                                    <delete dir="${project.build.directory}/arthas"/>
+                                    <unzip src="${project.build.directory}/arthas-packaging/arthas-bin.zip"
+                                           dest="${project.build.directory}/arthas">
+                                        <patternset>
+                                            <include name="arthas-*.jar"/>
+                                            <include name="logback.xml"/>
+                                            <include name="async-profiler/**"/>
+                                            <include name="lib/**"/>
+                                        </patternset>
+                                    </unzip>
+                                </target>
+                            </configuration>
+                        </execution>
                         <execution>
                             <id>download-swagger-ui</id>
                             <phase>prepare-package</phase>
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/descriptor/assembly.xml b/hugegraph-server/hugegraph-dist/src/assembly/descriptor/assembly.xml
index b7f9ee048..bf5db512f 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/descriptor/assembly.xml
+++ b/hugegraph-server/hugegraph-dist/src/assembly/descriptor/assembly.xml
@@ -23,6 +23,10 @@
     </formats>
 
     <fileSets>
+        <fileSet>
+            <directory>${project.build.directory}/arthas</directory>
+            <outputDirectory>lib/arthas</outputDirectory>
+        </fileSet>
         <fileSet>
             <directory>${assembly.static.dir}/bin</directory>
             <outputDirectory>bin</outputDirectory>
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/ArthasAttach.java b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/ArthasAttach.java
new file mode 100644
index 000000000..521d3536f
--- /dev/null
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/ArthasAttach.java
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.regex.Pattern;
+
+import com.sun.tools.attach.VirtualMachine;
+import com.taobao.arthas.core.config.FeatureCodec;
+
+class ArthasAttach {
+    public static void main(String[] args) throws Exception {
+        String ip = args[5];
+        InetAddress address = InetAddress.getByName(ip);
+        // Prefer native IPv4 loopback access for wildcard listeners, including dual-stack ::.
+        String connectIp = address.isAnyLocalAddress() ? "127.0.0.1" : ip;
+        Files.writeString(Path.of(args[4]).resolveSibling("connect-ip"), connectIp);
+        Path home = Path.of(args[1]);
+        Path top = Path.of(args[3]);
+        VirtualMachine vm = VirtualMachine.attach(args[0]);
+        Path configFile = null;
+        try {
+            Properties target = vm.getSystemProperties();
+            String command = target.getProperty("sun.java.command", "");
+            String main = "org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap";
+            boolean server = Files.isRegularFile(top.resolve("bin/start-hugegraph.sh")) &&
+                             (command.equals(main) || command.startsWith(main + " "));
+            boolean store = Files.isRegularFile(top.resolve("bin/start-hugegraph-store.sh")) &&
+                            (command.matches(Pattern.quote(top.resolve("lib/hg-store-node-").toString()) +
+                                             "[^/]+[.]jar(?: .*|$)") ||
+                             (command.matches("org[.]springframework[.]boot[.]loader[.]JarLauncher(?: .*|$)") &&
+                              "HugeGraphStore".equals(target.getProperty("name"))));
+            if (!server && !store) {
+                throw new IllegalArgumentException("PID does not identify a Server or Store in this distribution");
+            }
+            // Allow exit cleanup only after this distribution's target has been validated.
+            Files.createFile(Path.of(args[4]));
+            // FeatureCodec drops blank values; a properties file preserves them.
+            configFile = Files.createTempFile("hugegraph-arthas-", ".properties");
+            Properties settings = new Properties();
+            settings.setProperty("arthas.config.overrideAll", "true");
+            settings.setProperty("arthas.ip", ip);
+            settings.setProperty("arthas.telnetPort", args[2]);
+            settings.setProperty("arthas.httpPort", "-1");
+            settings.setProperty("arthas.mcpEndpoint", "");
+            settings.setProperty("arthas.tunnelServer", "");
+            settings.setProperty("arthas.disabledCommands", "");
+            settings.setProperty("arthas.sessionTimeout", "1800");
+            if (!args[6].isEmpty() || !args[7].isEmpty()) {
+                settings.setProperty("arthas.username", args[6].isEmpty() ? "arthas" : args[6]);
+            }
+            if (!args[7].isEmpty()) {
+                settings.setProperty("arthas.password", args[7]);
+            }
+            settings.setProperty("arthas.localConnectionNonAuth",
+                                 target.getProperty("arthas.localConnectionNonAuth", "true"));
+            try (OutputStream output = Files.newOutputStream(configFile)) {
+                settings.store(output, null);
+            }
+            Map<String, String> config = new LinkedHashMap<>();
+            config.put("config.overrideAll", "true");
+            config.put("config.location", configFile.toString());
+            config.put("home", home.toString());
+            String agentArgs = home.resolve("arthas-core.jar") + ";" +
+                               FeatureCodec.DEFAULT_COMMANDLINE_CODEC.toString(config);
+            vm.loadAgent(home.resolve("arthas-agent.jar").toString(),
+                         URLEncoder.encode(agentArgs, StandardCharsets.UTF_8));
+        } finally {
+            vm.detach();
+            if (configFile != null) {
+                Files.deleteIfExists(configFile);
+            }
+        }
+    }
+}
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/arthas.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/arthas.sh
new file mode 100755
index 000000000..3b20dcdc6
--- /dev/null
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/arthas.sh
@@ -0,0 +1,232 @@
+#!/bin/bash
+#
+# 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.
+#
+# Offline attachment to a running Server or Store using the bundled agent.
+set -euo pipefail
+
+fail() { echo "Error: $*" >&2; exit 1; }
+usage() {
+    echo "Usage: $0 [--pid PID] [--telnet-port PORT] [--target-ip ADDRESS] [--command COMMAND]"
+    echo "          [--username USER] [--password PASSWORD]"
+    echo "Run as the service OS user; defaults to bin/pid and loopback port 3658."
+    echo "New agents disable HTTP/MCP; use the existing telnet port for an active agent."
+    echo "Exiting this helper stops Arthas, including after --command or quit; the JVM keeps running."
+}
+
+BIN=$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+TOP=$(cd -P "$BIN/.." && pwd)
+pid=""
+port=3658
+target_ip=127.0.0.1
+username=""
+password=""
+command=""
+boot_args=()
+while [ "$#" -gt 0 ]; do
+    case "$1" in
+        --pid|--telnet-port|--target-ip|--command|--username|--password)
+            [ "$#" -ge 2 ] || fail "Missing value for $1"
+            case "$1" in
+                --pid) pid=$2 ;;
+                --telnet-port) port=$2 ;;
+                --target-ip) target_ip=$2 ;;
+                --command) command=$2 ;;
+                --username) username=$2 ;;
+                --password) password=$2 ;;
+            esac
+            shift 2 ;;
+        -h|--help) usage; exit 0 ;;
+        *) usage >&2; fail "Unknown option: $1" ;;
+    esac
+done
+# Native batch files contain one command per line.
+for credential in "$username" "$password"; do
+    [[ "$credential" != *$'\n'* && "$credential" != *$'\r'* ]] || fail "Credentials must be single-line values"
+done
+if [ -z "$pid" ]; then
+    [ -r "$BIN/pid" ] || fail "No bin/pid; start the service or supply --pid PID"
+    pid=$(cat "$BIN/pid")
+fi
+[[ "$pid" =~ ^[1-9][0-9]{0,9}$ ]] || fail "PID must be a positive integer"
+[[ "$port" =~ ^[1-9][0-9]{0,4}$ ]] && [ "$port" -le 65535 ] || fail "Invalid telnet port"
+[ -t 0 ] || [ -n "$command" ] || fail "Interactive use needs a terminal; use --command COMMAND"
+case "$target_ip" in
+    [Ll][Oo][Cc][Aa][Ll][Hh][Oo][Ss][Tt]) target_ip=127.0.0.1 ;;
+esac
+JAVA=${JAVA_HOME:+$JAVA_HOME/bin/}java
+ARTHAS="$TOP/lib/arthas"
+[ -f "$ARTHAS/arthas-boot.jar" ] || fail "Bundled Arthas files are missing from lib/arthas"
+# Keep the listener within this diagnosis session. Verify its PID before stopping
+# it, since an occupied port must never stop a different application's agent.
+state_dir=$(mktemp -d)
+client_pid=""
+attaching=true
+connect_ip=$target_ip
+quote_argument() {
+    local value=${1//\'/\'\\\'\'}
+    printf "'%s'" "$value"
+}
+write_batch() {
+    local file=$1
+    local commands=$2
+    {
+        if [ -n "$password" ]; then
+            printf 'auth --username %s %s\n' "$(quote_argument "${username:-arthas}")" "$(quote_argument "$password")"
+        fi
+        # Match arthas-client --command's semicolon-separated command syntax.
+        printf '%s\n' "${commands//;/$'\n'}"
+    } > "$file"
+}
+redact_auth_echo() {
+    local line
+    while IFS= read -r line || [ -n "$line" ]; do
+        case "$line" in
+            *'$ auth --username '*) printf '%s$ auth [credentials omitted]\n' "${line%%'$ auth --username '*}" ;;
+            *) printf '%s\n' "$line" ;;
+        esac
+    done
+}
+# The native client's -t option does not bound its final response wait.
+bounded_client() {
+    write_batch "$state_dir/cleanup.commands" "$1"
+    "$JAVA" -jar "$ARTHAS/arthas-client.jar" -t 5000 -f "$state_dir/cleanup.commands" "$connect_ip" "$port" &
+    local command_pid=$!
+    (sleep 10; kill -KILL "$command_pid" 2>/dev/null) >/dev/null 2>&1 &
+    local timer_pid=$!
+    local status=0
+    wait "$command_pid" || status=$?
+    kill -KILL "$timer_pid" 2>/dev/null || true
+    wait "$timer_pid" 2>/dev/null || true
+    return "$status"
+}
+format_endpoint() {
+    case "$connect_ip" in
+        *:*) printf '[%s]:%s' "$connect_ip" "$port" ;;
+        *) printf '%s:%s' "$connect_ip" "$port" ;;
+    esac
+}
+stop_agent() {
+    local session attempt agent_pid="" line endpoint rows=0
+    connect_ip=$(cat "$state_dir/connect-ip")
+    endpoint=$(format_endpoint)
+    if ! session=$(bounded_client session 2>&1); then
+        if [[ "$session" == *"Connection refused"* ]]; then
+            echo "Arthas listener closed at $endpoint" >&2
+            return 0
+        fi
+        if [[ "$session" == *"not permitted"* ]]; then
+            echo "Error: Arthas credentials rejected at $endpoint; agent left running" >&2
+        else
+            echo "Error: could not verify Arthas cleanup at $endpoint; agent left running" >&2
+        fi
+        return 1
+    fi
+    # AuthCommand echoes the username. Trust only one complete session table row.
+    while IFS= read -r line; do
+        if [[ "$line" =~ ^[[:space:]]*JAVA_PID[[:space:]]+([0-9]+)[[:space:]]*$ ]]; then
+            agent_pid=${BASH_REMATCH[1]}
+            rows=$((rows + 1))
+        fi
+    done <<< "$session"
+    if [ "$rows" -ne 1 ]; then
+        if [ "$rows" -eq 0 ] && [[ "$session" == *"not permitted"* ]]; then
+            echo "Error: Arthas credentials rejected at $endpoint; agent left running" >&2
+        else
+            echo "Error: could not verify an unambiguous Arthas PID at $endpoint; agent left running" >&2
+        fi
+        return 1
+    fi
+    if [ "$agent_pid" != "$pid" ]; then
+        echo "Error: Arthas at $endpoint belongs to PID $agent_pid, expected $pid; agent left running" >&2
+        return 1
+    fi
+    bounded_client stop >/dev/null 2>&1 || true
+    # Command errors can return zero; verify closure after asynchronous shutdown.
+    for attempt in 1 2 3; do
+        if ! session=$(bounded_client session 2>&1) && [[ "$session" == *"Connection refused"* ]]; then
+            echo "Arthas listener closed at $endpoint" >&2
+            return 0
+        fi
+        sleep 0.2
+    done
+    echo "Error: could not stop Arthas at $endpoint; check session/stop settings before retrying" >&2
+    return 1
+}
+cleanup() {
+    local status=$?
+    trap '' INT TERM HUP
+    trap - EXIT
+    if [ -n "$client_pid" ]; then
+        # Let an in-flight loadAgent finish before checking/closing its listener.
+        if [ "$attaching" = false ]; then
+            kill "$client_pid" 2>/dev/null || true
+        fi
+        # A queued second signal can interrupt wait even after traps are ignored.
+        while kill -0 "$client_pid" 2>/dev/null; do
+            wait "$client_pid" 2>/dev/null || true
+        done
+    fi
+    if [ -f "$state_dir/validated" ]; then
+        stop_agent || { [ "$status" -ne 0 ] || status=1; }
+    fi
+    rm -f "$state_dir/validated" "$state_dir/connect-ip" "$state_dir/cleanup.commands" "$state_dir/run.commands"
+    rmdir "$state_dir"
+    exit "$status"
+}
+trap cleanup EXIT
+trap 'trap "" INT TERM HUP; exit 130' INT
+trap 'trap "" INT TERM HUP; exit 143' TERM
+trap 'trap "" INT TERM HUP; exit 129' HUP
+echo "Waiting for PID $pid to answer the attach request..." >&2
+(
+    # Group signals must not end the client before target-side loadAgent finishes.
+    trap '' INT TERM HUP
+    exec "$JAVA" --add-modules jdk.attach --class-path "$ARTHAS/arthas-core.jar" \
+        "$BIN/ArthasAttach.java" "$pid" "$ARTHAS" "$port" "$TOP" "$state_dir/validated" "$target_ip" \
+        "$username" "$password"
+) <&0 &
+client_pid=$!
+wait "$client_pid"
+client_pid=""
+attaching=false
+connect_ip=$(cat "$state_dir/connect-ip")
+if ! session=$(bounded_client session 2>&1) && [[ "$session" == *"Connection refused"* ]]; then
+    echo "Error: no Arthas listener at $(format_endpoint);" \
+         "an existing agent may use another port (API default 8562); retry with --telnet-port" >&2
+    rm -f "$state_dir/validated"
+    exit 1
+fi
+if [ -n "$command" ]; then
+    if [ -n "$password" ]; then
+        write_batch "$state_dir/run.commands" "$command"
+        boot_args+=(--batch-file "$state_dir/run.commands")
+    else
+        boot_args+=(--command "$command")
+    fi
+fi
+[ -z "$username" ] || boot_args+=(--username "$username")
+[ -z "$password" ] || boot_args+=(--password "$password")
+boot_args+=(--arthas-home "$ARTHAS" --target-ip "$connect_ip" --telnet-port "$port" --http-port -1 "$pid")
+if [ -n "$password" ] && [ -n "$command" ]; then
+    # The native batch client echoes commands, including its auth command.
+    "$JAVA" -jar "$ARTHAS/arthas-boot.jar" "${boot_args[@]}" <&0 > >(redact_auth_echo) &
+else
+    "$JAVA" -jar "$ARTHAS/arthas-boot.jar" "${boot_args[@]}" <&0 &
+fi
+client_pid=$!
+wait "$client_pid"
+client_pid=""
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/gremlin-console.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/gremlin-console.sh
index 40ec2ff7d..eeee6b07c 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/gremlin-console.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/gremlin-console.sh
@@ -52,6 +52,7 @@ CP="$CP":$(find -L $LIB -name 'slf4j-log4j12*.jar' | sort | tr '\n' ':')
 CP="$CP":$(find -L $LIB -name 'hugegraph*.jar' | sort | tr '\n' ':')
 # Add the remaining jars in $BIN/../lib.
 CP="$CP":$(find -L $LIB -name '*.jar' \
+    \! -path "$LIB/arthas/*" \
                 \! -name 'hugegraph*' \
                 \! -name 'slf4j-log4j12*.jar' | sort | tr '\n' ':')
 # Add the jars in $BIN/../ext (at any subdirectory depth)
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh
index cdea1f8ce..3e0276ec0 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh
@@ -87,6 +87,7 @@ CP=$(find -L $LIB -name 'log4j-slf4j-impl*.jar' | sort | tr '\n' ':')
 CP="$CP":$(find -L $LIB -name 'hugegraph*.jar' | sort | tr '\n' ':')
 # Add the remaining jars in lib.
 CP="$CP":$(find -L $LIB -name '*.jar' \
+    \! -path "$LIB/arthas/*" \
     \! -name 'hugegraph*' \
     \! -name 'log4j-slf4j-impl*.jar' | sort | tr '\n' ':')
 # Add the jars in ext (at any subdirectory depth)
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh
index b1eb3438f..754a74cd6 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh
@@ -54,7 +54,8 @@ echo "Initializing HugeGraph Store..."
 
 # Build classpath with hugegraph*.jar first to avoid class loading conflicts
 CP=$(find -L "${LIB}" -name 'hugegraph*.jar' | sort | tr '\n' ':')
-CP="$CP":$(find -L "${LIB}" -name '*.jar' \! -name 'hugegraph*' | sort | tr '\n' ':')
+CP="$CP":$(find -L "${LIB}" -name '*.jar' \
+    \! -path "${LIB}/arthas/*" \! -name 'hugegraph*' | sort | tr '\n' ':')
 CP="$CP":$(find -L "${PLUGINS}" -name '*.jar' | sort | tr '\n' ':')
 source "$BIN/preload-topling.sh" || exit 1
 CP="${TOPLING_RUNTIME_CLASSPATH:+$TOPLING_RUNTIME_CLASSPATH:}$CP"
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties
index daeb092b7..be7b82a3b 100644
--- a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties
+++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/rest-server.properties
@@ -12,10 +12,20 @@ batch.max_write_ratio=80
 batch.max_write_threads=0
 
 # configuration of arthas
+# Legacy aliases: arthas.telnetPort, arthas.httpPort, arthas.disabledCommands.
+# The snake_case key takes precedence when both forms are configured.
+# Different values for both forms produce a warning when the agent is activated.
 arthas.telnet_port=8562
 arthas.http_port=8561
+# Defaults to local access. External binding (e.g. 0.0.0.0) exposes diagnostic commands;
+# enable Arthas native authentication and restrict network access before exposing it.
+# For API activation, set -Darthas.username=... -Darthas.password=... on the Server JVM.
+# These credentials also apply to bin/arthas.sh; pass matching --username/--password for authenticated cleanup.
+# Native loopback auth bypass is controlled by -Darthas.localConnectionNonAuth=false.
+# Restart to apply JVM options; passwords in JVM arguments may appear in process listings.
 arthas.ip=127.0.0.1
-arthas.disabled_commands=jad
+# Optional comma-separated disabled commands, for example jad,mc; empty allows all commands.
+arthas.disabled_commands=
 
 # authentication configs
 #auth.authenticator=org.apache.hugegraph.auth.StandardAuthenticator
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-arthas-cli.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-arthas-cli.sh
new file mode 100644
index 000000000..b4428ebff
--- /dev/null
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-arthas-cli.sh
@@ -0,0 +1,404 @@
+#!/bin/bash
+#
+# 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.
+#
+# Usage: bash test-arthas-cli.sh <built-server-or-store-distribution>
+# Real native attachment; all processes and files below belong to this test.
+set -euo pipefail
+[ "$#" -eq 1 ] || { echo "Supply a built Server or Store distribution" >&2; exit 1; }
+python3 - "$1" <<'PY'
+import os
+import pathlib
+import pty
+import re
+import select
+import shlex
+import shutil
+import signal
+import socket
+import subprocess
+import sys
+import tempfile
+import time
+import zipfile
+
+built = pathlib.Path(sys.argv[1]).resolve()
+store = (built / "bin/start-hugegraph-store.sh").is_file()
+component = "Store" if store else "Server"
+startup = "start-hugegraph-store.sh" if store else "start-hugegraph.sh"
+other_startup = "start-hugegraph.sh" if store else "start-hugegraph-store.sh"
+assert (built / "bin" / startup).is_file(), f"Not a Server or Store distribution: {built}"
+java_home = os.environ.get("JAVA_HOME")
+java = str(pathlib.Path(java_home) / "bin/java") if java_home else "java"
+javac = str(pathlib.Path(java_home) / "bin/javac") if java_home else "javac"
+with tempfile.TemporaryDirectory(prefix="arthas-test-") as work:
+    root = (pathlib.Path(work) / "native fixture").resolve()
+    for name in ("bin", "lib", "classes", "client-temp"):
+        (root / name).mkdir(parents=True)
+    for name in ("arthas.sh", "ArthasAttach.java"):
+        shutil.copy(built / "bin" / name, root / "bin" / name)
+    (root / "lib/arthas").symlink_to(built / "lib/arthas", target_is_directory=True)
+    (root / "bin" / startup).touch()
+    main = ("org.apache.hugegraph.store.node.StoreNodeApplication" if store else
+            "org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap")
+    package, name = main.rsplit(".", 1)
+    source = root / f"{name}.java"
+    source.write_text(f"package {package}; public class {name} {{ "
+                      "public static void main(String[] args) throws Exception { "
+                      "Thread.sleep(180000); }}")
+    subprocess.run([javac, "-d", str(root / "classes"), str(source)], check=True)
+    if store:
+        # Store starts from an executable jar; exercise that PID validation path
+        # using the built distribution's real bundled agent and attach helper.
+        fixture_jar = root / "lib/hg-store-node-fixture.jar"
+        with zipfile.ZipFile(fixture_jar, "w") as jar:
+            jar.writestr("META-INF/MANIFEST.MF", f"Manifest-Version: 1.0\nMain-Class: {main}\n\n")
+            for compiled in (root / "classes").rglob("*.class"):
+                jar.write(compiled, compiled.relative_to(root / "classes"))
+        launch = ["-jar", str(fixture_jar)]
+    else:
+        launch = ["-cp", str(root / "classes"), main]
+    reservations = [socket.socket() for _ in range(3)]
+    for reservation in reservations:
+        reservation.bind(("127.0.0.1", 0))
+    safe_port, unsafe_telnet, unsafe_http = [s.getsockname()[1] for s in reservations]
+    for reservation in reservations:
+        reservation.close()
+    tunnel = socket.socket()
+    tunnel.bind(("127.0.0.1", 0))
+    tunnel.listen()
+    tunnel.settimeout(0.5)
+    tunnel_url = f"ws://127.0.0.1:{tunnel.getsockname()[1]}/ws"
+    unsafe = root / "unsafe.properties"
+    unsafe.write_text(f"arthas.config.overrideAll=true\narthas.ip=0.0.0.0\n"
+                      f"arthas.telnetPort={unsafe_telnet}\narthas.httpPort={unsafe_http}\n"
+                      f"arthas.mcpEndpoint=/mcp\narthas.tunnelServer={tunnel_url}\n"
+                      "arthas.disabledCommands=stop,session\n")
+    target_env = dict(os.environ, ARTHAS_CONFIG_LOCATION=str(unsafe),
+                      ARTHAS_CONFIG_OVERRIDE_ALL="true", ARTHAS_CONFIG_OVERRIDEALL="true", ARTHAS_IP="0.0.0.0",
+                      ARTHAS_TUNNELSERVER=tunnel_url, ARTHAS_TUNNEL_SERVER=tunnel_url,
+                      ARTHAS_DISABLEDCOMMANDS="stop,session")
+    target_env["arthas.config.location"] = str(unsafe)
+    target_env["arthas.tunnelServer"] = tunnel_url
+    client_env = dict(os.environ, TMPDIR=str(root / "client-temp"), JAVA_TOOL_OPTIONS=f'-Djava.io.tmpdir="{root / "client-temp"}"')
+    script = ["bash", str(root / "bin/arthas.sh")]
+    client = script + ["--telnet-port", str(safe_port)]
+
+    def run(arguments, success=True):
+        result = subprocess.run(arguments, env=client_env, text=True,
+                                stdout=subprocess.PIPE, stderr=subprocess.STDOUT, input="", timeout=45)
+        assert (result.returncode == 0) == success, result.stdout
+        return result.stdout
+
+    def can_connect(host, port):
+        try:
+            with socket.create_connection((host, port), timeout=1):
+                return True
+        except OSError:
+            return False
+
+    def wait_port(opened, host="127.0.0.1"):
+        deadline = time.monotonic() + 15
+        while can_connect(host, safe_port) != opened:
+            assert time.monotonic() < deadline, f"Listener did not become {'open' if opened else 'closed'}"
+            time.sleep(0.1)
+
+    class Terminal:
+        # fork gives the helper a real controlling terminal: closing its master
+        # reproduces terminal hangup, including the kernel's process-group HUP.
+        def __init__(self, shell=False, options=()):
+            self.pid, self.master = pty.fork()
+            if self.pid == 0:
+                arguments = ["bash", "--noprofile", "--norc", "-i"] if shell else client + list(options)
+                os.execvpe(arguments[0], arguments, client_env)
+            self.returncode = None
+            self.output = ""
+            if shell:
+                os.write(self.master, (shlex.join(client) + "\n").encode())
+
+        def poll(self):
+            if self.returncode is None:
+                child, status = os.waitpid(self.pid, os.WNOHANG)
+                if child:
+                    self.returncode = os.waitstatus_to_exitcode(status)
+            return self.returncode
+
+        def drain(self):
+            if self.master is not None and select.select([self.master], [], [], 0)[0]:
+                try:
+                    output = os.read(self.master, 65536).decode(errors="replace")
+                    self.output += output
+                    log.write(output)
+                    log.flush()
+                except OSError:
+                    pass  # The slave closes when the helper exits.
+
+        def ready(self):
+            deadline = time.monotonic() + 15
+            while "[arthas@" not in self.output:
+                self.drain()
+                assert self.poll() is None and time.monotonic() < deadline, self.output
+                time.sleep(0.02)
+
+        def wait(self, timeout=30):
+            deadline = time.monotonic() + timeout
+            while self.poll() is None:
+                self.drain()
+                assert time.monotonic() < deadline, "Interactive helper did not exit"
+                time.sleep(0.02)
+            self.drain()
+            if self.master is not None:
+                os.close(self.master)
+                self.master = None
+            return self.returncode
+
+    interactive = None
+    with (root / "jvm.log").open("w") as log:
+        process = subprocess.Popen([java, "-Darthas.config.location=" + str(unsafe),
+                                    "-Darthas.config.overrideAll=true", "-Darthas.ip=0.0.0.0",
+                                    "-Darthas.tunnelServer=" + tunnel_url,
+                                    "-Darthas.disabledCommands=stop,session",
+                                    *launch, "fixture"], env=target_env, stdout=log, stderr=subprocess.STDOUT)
+        try:
+            (root / "bin/pid").write_text(str(process.pid))
+            time.sleep(1)
+            for options in (["--pid", "-1"], ["--pid", "1; unsafe"], ["--pid", "9999999999"],
+                            ["--telnet-port", "65536"], ["--target-ip", "::not-an-ip"],
+                            ["--username", "line\nbreak"],
+                            ["--http-port", "8563"], ["--tunnel-server", tunnel_url]):
+                output = run(script + options + ["--command", "version"], success=False)
+                if options[0] == "--username":
+                    assert "single-line values" in output and "Waiting for PID" not in output, output
+                    assert not can_connect("127.0.0.1", safe_port)
+            # EOF and piped input must fail before attaching; batch callers need --command.
+            for input_text in ("", "version\n"):
+                result = subprocess.run(client, env=client_env, input=input_text, text=True,
+                                        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10)
+                assert result.returncode != 0 and "use --command" in result.stdout, result.stdout
+                assert not can_connect("127.0.0.1", safe_port)
+            # A live PID from a different component must not receive the agent.
+            (root / "bin" / startup).unlink()
+            (root / "bin" / other_startup).touch()
+            run(client + ["--command", "version"], success=False)
+            (root / "bin" / other_startup).unlink()
+            (root / "bin" / startup).touch()
+            output = run(client + ["--pid", str(process.pid), "--command", "version"])
+            assert re.search(r"\n4\.1\.5\s*\n", output), output
+            assert (f"Process {process.pid} already using port {safe_port}" in output or
+                    f"The target process already listen port {safe_port}, skip attach." in output), output
+            wait_port(False)
+            assert process.poll() is None, "One-time diagnosis stopped the application JVM"
+            # Keep a real interactive client open while inspecting the live agent.
+            interactive = Terminal()
+            wait_port(True)
+            assert not can_connect("127.0.0.1", unsafe_telnet)
+            assert not can_connect("127.0.0.1", unsafe_http)
+            try:
+                connection, address = tunnel.accept()
+            except socket.timeout:
+                pass
+            else:
+                connection.close()
+                raise AssertionError(f"Arthas contacted the overridden tunnel endpoint: {address}")
+            # Inspect actual bind addresses; VPNs may redirect connection probes.
+            if sys.platform.startswith("linux"):
+                inodes = set()
+                for descriptor in pathlib.Path(f"/proc/{process.pid}/fd").iterdir():
+                    try:
+                        link = os.readlink(descriptor)
+                    except FileNotFoundError:
+                        continue
+                    if link.startswith("socket:["):
+                        inodes.add(link[8:-1])
+                addresses = []
+                for table in ("tcp", "tcp6"):
+                    for line in pathlib.Path(f"/proc/{process.pid}/net/{table}").read_text().splitlines()[1:]:
+                        fields = line.split()
+                        address, port = fields[1].split(":")
+                        if fields[3] == "0A" and fields[9] in inodes:
+                            addresses.append((address, int(port, 16)))
+                assert len(addresses) == 1 and addresses[0][1] == safe_port, addresses
+                assert addresses[0][0] in ("0100007F", "0000000000000000FFFF00000100007F"), addresses
+            else:
+                listeners = subprocess.check_output(["lsof", "-nP", "-a", "-p", str(process.pid),
+                                                     "-iTCP", "-sTCP:LISTEN"], text=True).splitlines()[1:]
+                assert len(listeners) == 1 and f"127.0.0.1:{safe_port} " in listeners[0], listeners
+            os.write(interactive.master, b"quit\n")
+            assert interactive.wait(45) == 0, (root / "jvm.log").read_text()
+            wait_port(False)
+            # Interactive Bash propagates the terminal HUP to its foreground job.
+            # Closing a direct helper PTY alone does not reproduce that signal sequence.
+            interactive = Terminal(shell=True)
+            wait_port(True)
+            interactive.ready()
+            os.close(interactive.master)
+            interactive.master = None
+            status = interactive.wait()
+            assert status in (-signal.SIGHUP, 128 + signal.SIGHUP), status
+            wait_port(False)
+            assert process.poll() is None, "Terminal hangup stopped the application JVM"
+            # Signals sent only to the helper must also clean up its child/client.
+            for termination in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT):
+                interactive = Terminal()
+                if termination == signal.SIGTERM:
+                    # Terminate during source compilation/attachment, before a listener exists.
+                    time.sleep(0.2)
+                else:
+                    wait_port(True)
+                    interactive.ready()
+                os.kill(interactive.pid, termination)
+                if termination != signal.SIGTERM:
+                    # Repeated/mixed signals during cleanup must not interrupt stop_agent.
+                    for repeat in (signal.SIGHUP, signal.SIGINT, signal.SIGTERM) * 5:
+                        time.sleep(0.02)
+                        if interactive.poll() is None:
+                            os.kill(interactive.pid, repeat)
+                assert interactive.wait() == 128 + termination, (root / "jvm.log").read_text()
+                wait_port(False)
+                assert process.poll() is None, "Terminal/signal cleanup stopped the application JVM"
+            # Linux mktemp honors TMPDIR, exposing only this fixture's marker.
+            # macOS mktemp ignores it; the other terminal/signal cases run there.
+            if sys.platform.startswith("linux"):
+                # Pause after validation so loadAgent cannot finish before the
+                # signal reaches both the helper and its attach JVM.
+                for termination in (signal.SIGTERM, signal.SIGHUP):
+                    interactive = Terminal()
+                    deadline = time.monotonic() + 15
+                    while not list((root / "client-temp").glob("tmp.*/validated")):
+                        interactive.drain()
+                        assert interactive.poll() is None and time.monotonic() < deadline, interactive.output
+                        time.sleep(0.001)
+                    os.kill(process.pid, signal.SIGSTOP)
+                    try:
+                        assert not can_connect("127.0.0.1", safe_port), "Agent started before the attach pause"
+                        # Queue the attach request before signaling the group.
+                        time.sleep(0.1)
+                        os.killpg(interactive.pid, termination)
+                        if termination == signal.SIGHUP:
+                            # Kernel and interactive Bash can deliver HUP almost together.
+                            time.sleep(0.00006)
+                            os.killpg(interactive.pid, termination)
+                        time.sleep(1)
+                        assert interactive.poll() is None, "Group signal interrupted in-flight attachment"
+                    finally:
+                        os.kill(process.pid, signal.SIGCONT)
+                    assert interactive.wait() == 128 + termination, interactive.output
+                    wait_port(False)
+                    assert process.poll() is None, "Group signal stopped the application JVM"
+            # macOS requires an explicit interface alias for 127.0.0.2; Linux
+            # routes all of 127/8 locally and exercises that address in CI.
+            hosts = ("127.0.0.2", "::1", "localhost") if sys.platform.startswith("linux") else ("::1", "localhost")
+            for host in (*hosts, "0.0.0.0", "::"):
+                output = run(client + ["--target-ip", host, "--command", "version"])
+                assert re.search(r"\n4\.1\.5\s*\n", output), output
+                connect_host = {"localhost": "127.0.0.1", "0.0.0.0": "127.0.0.1", "::": "127.0.0.1"}.get(host, host)
+                endpoint = f"[{connect_host}]:{safe_port}" if ":" in connect_host else f"{connect_host}:{safe_port}"
+                assert output.count(f"Arthas listener closed at {endpoint}") == 1, output
+                wait_port(False, connect_host)
+            run(client + ["--command", "stop"])
+            wait_port(False)
+            # Disable native local authentication bypass for the following
+            # attachments; earlier signal cases exercise native no-auth defaults.
+            run(client + ["--command", "sysprop arthas.localConnectionNonAuth false"])
+            wait_port(False)
+            password = "p a;ss'\"\\word"
+            auth = ["--username", "operator name", "--password", password]
+            for options in (auth, ["--password", password]):
+                output = run(client + options + ["--command", "version"])
+                assert re.search(r"\n4\.1\.5\s*\n", output), output
+                assert password not in output and "auth --username" not in output, output
+                wait_port(False)
+                assert process.poll() is None, "Authenticated command stopped the application JVM"
+            interactive = Terminal(options=auth)
+            wait_port(True)
+            interactive.ready()
+            # An independent unauthenticated loopback client must be rejected.
+            unauthenticated = run([java, "-jar", str(root / "lib/arthas/arthas-client.jar"),
+                                   "-c", "version", "127.0.0.1", str(safe_port)])
+            assert "command not permitted" in unauthenticated, unauthenticated
+            assert not re.search(r"\n4\.1\.5\s*\n", unauthenticated), unauthenticated
+            # A running agent retains its credentials; invalid credentials must
+            # fail diagnosis/cleanup without stopping the existing listener.
+            wrong = run(client + ["--username", "operator name", "--password", "incorrect ; secret",
+                                  "--command", "version"], success=False)
+            assert "command not permitted" in wrong and "incorrect ; secret" not in wrong, wrong
+            assert "credentials rejected" in wrong and "agent left running" in wrong, wrong
+            wait_port(True)
+            quoted_password = "'" + password.replace("'", "'\\''") + "'"
+            os.write(interactive.master,
+                     f'auth --username "operator name" {quoted_password}\nversion\nquit\n'.encode())
+            assert interactive.wait(45) == 0, interactive.output
+            assert re.search(r"[\r\n]4\.1\.5\s*[\r\n]", interactive.output), interactive.output
+            wait_port(False)
+            assert process.poll() is None, "Authenticated quit stopped the application JVM"
+            # Authentication echoes must not let one JVM impersonate another
+            # agent's session row. Both JVMs are valid targets in this fixture.
+            other = subprocess.Popen([java, "-Darthas.localConnectionNonAuth=false", *launch, "other fixture"],
+                                     env=target_env, stdout=log, stderr=subprocess.STDOUT)
+            try:
+                time.sleep(1)
+                impersonating = ["--username", f"prefix JAVA_PID {process.pid} suffix",
+                                 "--password", "mismatch ; secret"]
+                interactive = Terminal(options=["--pid", str(other.pid), *impersonating])
+                wait_port(True)
+                interactive.ready()
+                missing = run(script + ["--pid", str(other.pid), "--telnet-port", str(unsafe_http),
+                                        *impersonating, "--command", "version"], success=False)
+                assert f"no Arthas listener at 127.0.0.1:{unsafe_http}" in missing, missing
+                assert "API default 8562" in missing and "retry with --telnet-port" in missing, missing
+                assert "Arthas listener closed at" not in missing, missing
+                assert can_connect("127.0.0.1", safe_port), "Missing-port diagnosis stopped the existing agent"
+                mismatch = run(client + ["--pid", str(process.pid), *impersonating,
+                                         "--command", "version"], success=False)
+                assert f"belongs to PID {other.pid}, expected {process.pid}" in mismatch, mismatch
+                assert can_connect("127.0.0.1", safe_port), "PID mismatch stopped another JVM's agent"
+                assert process.poll() is None and other.poll() is None
+                # This helper owns the other JVM. The misleading username must
+                # also not prevent its legitimate exit cleanup.
+                os.kill(interactive.pid, signal.SIGTERM)
+                assert interactive.wait() == 128 + signal.SIGTERM, interactive.output
+                wait_port(False)
+                assert "Arthas listener closed at" in interactive.output, interactive.output
+                assert other.poll() is None, "Agent cleanup stopped the other application JVM"
+            finally:
+                if interactive is not None and interactive.poll() is None:
+                    os.kill(interactive.pid, signal.SIGKILL)
+                    interactive.wait(10)
+                other.terminate()
+                try:
+                    other.wait(timeout=10)
+                except subprocess.TimeoutExpired:
+                    other.kill()
+                    other.wait()
+            assert not can_connect("127.0.0.1", safe_port)
+            assert not list((root / "client-temp").glob("hugegraph-arthas-*.properties"))
+            assert not list((root / "client-temp").glob("tmp.*")), "Helper state was not removed"
+            print(f"PASS: {component} PID/argument boundaries, hostile config, default loopback bind, no tunnel, "
+                  "command/quit/terminal/group/repeated-signal cleanup, explicit binds/authentication without restart")
+        finally:
+            if interactive is not None and interactive.poll() is None:
+                os.kill(interactive.pid, signal.SIGKILL)
+                interactive.wait(10)
+            tunnel.close()
+            if process.poll() is None:
+                os.kill(process.pid, signal.SIGCONT)
+            process.terminate()
+            try:
+                process.wait(timeout=10)
+            except subprocess.TimeoutExpired:
+                process.kill()
+                process.wait()
+PY
diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh
index bf7fd6453..dffc44324 100755
--- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh
+++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh
@@ -488,6 +488,7 @@ assert required_modules(pd_job) == {
 assert_order(store_job, [
     "mvn clean package",
     "mvn editorconfig:check -pl hugegraph-store/hg-store-test -am -ntp",
+    "-Dtest=StoredRowIngressTest,ArthasSecurityTest",
     "-P store-common-test -Djacoco.sessionId=store-common-test",
     "-P store-client-test -Djacoco.sessionId=store-client-test",
     "-P store-rocksdb-test -Djacoco.sessionId=store-rocksdb-test",
@@ -517,6 +518,7 @@ assert reports_for_option(store_job, "--require-test-report") == {
     "TEST-org.apache.hugegraph.store.core.CoreSuiteTest.xml",
     "TEST-org.apache.hugegraph.store.service.ServerSuiteTest.xml",
     "TEST-org.apache.hugegraph.store.business.StoredRowIngressTest.xml",
+    "TEST-org.apache.hugegraph.store.node.controller.ArthasSecurityTest.xml",
 }
 assert not reports_for_option(store_job, "--require-suite-report")
 assert values_for_option(store_job, "--require-session") == (selected_profiles(store_job, "store") | {"store-node-test"})
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ArthasApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ArthasApiTest.java
index ab7e49e67..89e83b7bb 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ArthasApiTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/ArthasApiTest.java
@@ -19,10 +19,10 @@ package org.apache.hugegraph.api;
 
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 
 import org.apache.hugegraph.util.JsonUtil;
 import org.junit.Assert;
-import org.junit.Before;
 import org.junit.Test;
 
 import com.google.common.collect.ImmutableMap;
@@ -35,7 +35,7 @@ public class ArthasApiTest extends BaseApiTest {
     private static final String ARTHAS_API_BASE_URL = "http://127.0.0.1:8561";
     private static final String ARTHAS_API_PATH = "/api";
 
-    @Before
+    @Test
     public void testArthasStart() {
         Response r = client().put(ARTHAS_START_PATH, "", "", ImmutableMap.of());
         String result = assertResponseStatus(200, r);
@@ -44,10 +44,39 @@ public class ArthasApiTest extends BaseApiTest {
         assertJsonContains(result, "arthas.httpPort");
         assertJsonContains(result, "arthas.ip");
         assertJsonContains(result, "arthas.disabledCommands");
+        Assert.assertEquals("127.0.0.1", assertJsonContains(result, "arthas.ip"));
+    }
+
+    @Test
+    public void testAnonymousCannotActivate() {
+        RestClient anonymous = new RestClient(BASE_URL, false);
+        try {
+            assertResponseStatus(401, anonymous.put(ARTHAS_START_PATH, "", "", ImmutableMap.of()));
+        } finally {
+            anonymous.close();
+        }
+    }
+
+    @Test
+    public void testNonAdminCannotActivate() {
+        String username = "arthas_user_" + UUID.randomUUID().toString().replace("-", "");
+        String usersPath = "graphspaces/DEFAULT/auth/users";
+        String userBody = JsonUtil.toJson(ImmutableMap.of("user_name", username,
+                                                         "user_password", "Arthas1234"));
+        String created = assertResponseStatus(201, client().post(usersPath, userBody));
+        String userId = assertJsonContains(created, "id");
+        RestClient user = new RestClient(BASE_URL, username, "Arthas1234");
+        try {
+            assertResponseStatus(403, user.put(ARTHAS_START_PATH, "", "", ImmutableMap.of()));
+        } finally {
+            user.close();
+            assertResponseStatus(204, client().delete(usersPath, userId));
+        }
     }
 
     @Test
     public void testArthasApi() {
+        testArthasStart();
         RestClient arthasApiClient = new RestClient(ARTHAS_API_BASE_URL, false);
         try {
             // A one-time command must succeed through its automatic session cleanup.
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/arthas/ArthasConfigTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/arthas/ArthasConfigTest.java
new file mode 100644
index 000000000..685086e84
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/arthas/ArthasConfigTest.java
@@ -0,0 +1,244 @@
+/*
+ * 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.hugegraph.api.arthas;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hugegraph.api.filter.AuthenticationFilter.Authorizer;
+import org.apache.hugegraph.auth.HugeAuthenticator.User;
+import org.apache.hugegraph.auth.HugePermission;
+import org.apache.hugegraph.auth.RolePermission;
+import org.apache.hugegraph.config.ConfigException;
+import org.apache.hugegraph.config.HugeConfig;
+import org.apache.hugegraph.config.OptionSpace;
+import org.apache.hugegraph.config.ServerOptions;
+import org.apache.hugegraph.core.GraphManager;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.Logger;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import jakarta.ws.rs.ForbiddenException;
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.SecurityContext;
+import jakarta.ws.rs.core.UriInfo;
+
+public class ArthasConfigTest {
+
+    private final List<LogEvent> events = new ArrayList<>();
+    private Logger logger;
+    private Level previousLevel;
+    private Appender appender;
+
+    @Before
+    public void captureWarnings() {
+        this.appender = Mockito.mock(Appender.class);
+        Mockito.when(this.appender.getName()).thenReturn("ArthasConfigTestAppender");
+        Mockito.when(this.appender.isStarted()).thenReturn(true);
+        Mockito.doAnswer(invocation -> {
+            LogEvent event = invocation.getArgument(0);
+            this.events.add(event.toImmutable());
+            return null;
+        }).when(this.appender).append(Mockito.any(LogEvent.class));
+        this.logger = (Logger) LogManager.getLogger(ArthasAPI.class);
+        this.previousLevel = this.logger.getLevel();
+        this.logger.setLevel(Level.WARN);
+        this.logger.addAppender(this.appender);
+    }
+
+    @After
+    public void restoreLogger() {
+        this.logger.removeAppender(this.appender);
+        this.logger.setLevel(this.previousLevel);
+    }
+
+    @BeforeClass
+    public static void registerOptions() {
+        OptionSpace.register("server", ServerOptions.instance());
+    }
+
+    @Test
+    public void testDefaults() {
+        Map<String, String> config = ArthasAPI.arthasConfig(new HugeConfig(Collections.emptyMap()));
+        assertOptions(config, "8562", "8561", "");
+        Assert.assertEquals("127.0.0.1", config.get("arthas.ip"));
+        Assert.assertTrue(this.events.isEmpty());
+    }
+
+    @Test
+    public void testDistributionKeys() {
+        Map<String, Object> properties = properties("telnet_port", "http_port", "disabled_commands");
+        properties.put("arthas.ip", "127.0.0.1");
+        Map<String, String> config = ArthasAPI.arthasConfig(new HugeConfig(properties));
+        assertOptions(config, "18562", "18561", "jad,stop");
+        Assert.assertEquals("127.0.0.1", config.get("arthas.ip"));
+        Assert.assertTrue(this.events.isEmpty());
+    }
+
+    @Test
+    public void testLegacyKeys() {
+        Map<String, Object> properties = properties("telnetPort", "httpPort", "disabledCommands");
+        assertOptions(ArthasAPI.arthasConfig(new HugeConfig(properties)), "18562", "18561", "jad,stop");
+        Assert.assertTrue(this.events.isEmpty());
+    }
+
+    @Test
+    public void testCanonicalKeysTakePrecedence() {
+        Map<String, Object> properties = properties("telnet_port", "http_port", "disabled_commands");
+        properties.put("arthas.telnetPort", "28562");
+        properties.put("arthas.httpPort", "28561");
+        properties.put("arthas.disabledCommands", "stop");
+        assertOptions(ArthasAPI.arthasConfig(new HugeConfig(properties)), "18562", "18561", "jad,stop");
+        Assert.assertEquals(3, this.events.size());
+        assertConflictWarning(0, "telnet_port", "telnetPort");
+        assertConflictWarning(1, "http_port", "httpPort");
+        assertConflictWarning(2, "disabled_commands", "disabledCommands");
+    }
+
+    @Test
+    public void testExplicitCanonicalDefaultsTakePrecedence() {
+        Map<String, Object> properties = properties("telnetPort", "httpPort", "disabledCommands");
+        properties.put("arthas.telnet_port", "8562");
+        properties.put("arthas.http_port", "8561");
+        properties.put("arthas.disabled_commands", "");
+        assertOptions(ArthasAPI.arthasConfig(new HugeConfig(properties)), "8562", "8561", "");
+        Assert.assertEquals(3, this.events.size());
+        assertConflictWarning(2, "disabled_commands", "disabledCommands");
+    }
+
+    @Test
+    public void testMatchingCanonicalAndLegacyKeysDoNotWarn() {
+        Map<String, Object> properties = properties("telnet_port", "http_port", "disabled_commands");
+        properties.putAll(properties("telnetPort", "httpPort", "disabledCommands"));
+        assertOptions(ArthasAPI.arthasConfig(new HugeConfig(properties)), "18562", "18561", "jad,stop");
+        Assert.assertTrue(this.events.isEmpty());
+    }
+
+    @Test
+    public void testCanonicalAndLegacyPortsAndIpRejectEmptyValues() {
+        for (String key : new String[]{"telnet_port", "telnetPort", "http_port", "httpPort", "ip"}) {
+            Assert.assertThrows(ConfigException.class,
+                                () -> new HugeConfig(Collections.singletonMap("arthas." + key, "")));
+        }
+    }
+
+    @Test
+    public void testCanonicalAndLegacyDisabledCommandsCanBeEmpty() {
+        for (String key : new String[]{"disabled_commands", "disabledCommands"}) {
+            Map<String, String> config = ArthasAPI.arthasConfig(
+                    new HugeConfig(Collections.singletonMap("arthas." + key, "")));
+            assertOptions(config, "8562", "8561", "");
+        }
+    }
+
+    @Test
+    public void testExplicitBindAddresses() {
+        for (String ip : new String[]{"127.0.0.1", "127.0.0.2", "::1", "localhost",
+                                      "0.0.0.0", "::", "192.0.2.1", "2001:db8::1", "fe80::1%1"}) {
+            Map<String, String> config = ArthasAPI.arthasConfig(
+                    new HugeConfig(Collections.singletonMap("arthas.ip", ip)));
+            Assert.assertEquals("localhost".equals(ip) ? "127.0.0.1" : ip, config.get("arthas.ip"));
+        }
+    }
+
+    @Test
+    public void testInvalidBindAddressesAreRejected() {
+        for (String ip : new String[]{"2001:db8::xyz", "2001:db8:::1", "[::1", "0.0.0.0:3658"}) {
+            HugeConfig config = new HugeConfig(Collections.singletonMap("arthas.ip", ip));
+            Assert.assertThrows(IllegalArgumentException.class, () -> ArthasAPI.arthasConfig(config));
+        }
+    }
+
+    @Test
+    public void testNoAuthCannotActivateWithAnonymousAdminRole() {
+        GraphManager manager = Mockito.mock(GraphManager.class);
+        SecurityContext context = authorizer(manager, User.ANONYMOUS);
+        Assert.assertTrue(context.isUserInRole("admin"));
+        Throwable failure = Assert.assertThrows(ForbiddenException.class,
+                                               () -> new ArthasAPI().startArthas(manager, context));
+        Assert.assertTrue(failure.getMessage().contains("bin/arthas.sh"));
+    }
+
+    @Test
+    public void testAuthenticatedNonAdminCannotActivate() {
+        GraphManager manager = Mockito.mock(GraphManager.class);
+        Mockito.when(manager.requireAuthentication()).thenReturn(true);
+        User user = new User("graph_reader", RolePermission.role("DEFAULT", "hugegraph", HugePermission.READ));
+        SecurityContext context = authorizer(manager, user);
+        Assert.assertThrows(ForbiddenException.class,
+                            () -> new ArthasAPI().startArthas(manager, context));
+    }
+
+    @Test
+    public void testAuthenticatedAdminIsAllowed() {
+        GraphManager manager = Mockito.mock(GraphManager.class);
+        Mockito.when(manager.requireAuthentication()).thenReturn(true);
+        ArthasAPI.checkAccess(manager, authorizer(manager, User.ADMIN));
+    }
+
+    @Test
+    public void testAnonymousOrMissingPrincipalIsRejectedEvenInAuthMode() {
+        GraphManager manager = Mockito.mock(GraphManager.class);
+        Mockito.when(manager.requireAuthentication()).thenReturn(true);
+        Assert.assertThrows(ForbiddenException.class,
+                            () -> new ArthasAPI().startArthas(manager, authorizer(manager, User.ANONYMOUS)));
+        Assert.assertThrows(ForbiddenException.class,
+                            () -> new ArthasAPI().startArthas(manager, Mockito.mock(SecurityContext.class)));
+    }
+
+    private static SecurityContext authorizer(GraphManager manager, User user) {
+        UriInfo uri = Mockito.mock(UriInfo.class);
+        Mockito.when(uri.getPathParameters()).thenReturn(new MultivaluedHashMap<>());
+        return new Authorizer(manager, user, uri);
+    }
+
+    private void assertConflictWarning(int index, String canonical, String legacy) {
+        LogEvent event = this.events.get(index);
+        Assert.assertEquals(Level.WARN, event.getLevel());
+        Assert.assertEquals(String.format("Conflicting Arthas options 'arthas.%s' and 'arthas.%s': " +
+                                         "'arthas.%s' takes precedence", canonical, legacy, canonical),
+                            event.getMessage().getFormattedMessage());
+    }
+
+    private static Map<String, Object> properties(String telnet, String http, String commands) {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put("arthas." + telnet, "18562");
+        properties.put("arthas." + http, "18561");
+        properties.put("arthas." + commands, "jad,stop");
+        return properties;
+    }
+
+    private static void assertOptions(Map<String, String> config, String telnet, String http,
+                                      String commands) {
+        Assert.assertEquals(4, config.size());
+        Assert.assertEquals(telnet, config.get("arthas.telnetPort"));
+        Assert.assertEquals(http, config.get("arthas.httpPort"));
+        Assert.assertEquals(commands, config.get("arthas.disabledCommands"));
+    }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
index 721937223..ee1c268dd 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
@@ -22,6 +22,7 @@ import org.apache.hugegraph.unit.core.LoadingModeTest;
 import org.apache.hugegraph.GraphDropPendingTest;
 import org.apache.hugegraph.StandardTaskAdmissionTest;
 import org.apache.hugegraph.DistributedTaskAdmissionTest;
+import org.apache.hugegraph.api.arthas.ArthasConfigTest;
 import org.apache.hugegraph.api.auth.GraphSpaceAuthPayloadTest;
 import org.apache.hugegraph.api.auth.GraphSpaceGroupAPITest;
 import org.apache.hugegraph.api.cypher.CypherClientTest;
@@ -151,6 +152,7 @@ import org.junit.runners.Suite;
         LoadDetectFilterTest.class,
         LoginAPITest.class,
         PathFilterTest.class,
+        ArthasConfigTest.class,
         VertexAuthorizationTest.class,
 
         /* api cypher */
diff --git a/hugegraph-server/pom.xml b/hugegraph-server/pom.xml
index 5e6da3324..7fff054fe 100644
--- a/hugegraph-server/pom.xml
+++ b/hugegraph-server/pom.xml
@@ -53,7 +53,6 @@
         <checkstyle.version>8.45</checkstyle.version>
         <protobuf.version>3.21.7</protobuf.version>
         <jmh.version>1.36</jmh.version>
-        <arthas.version>4.1.5</arthas.version>
         <swagger.version>2.2.18</swagger.version>
     </properties>
 
diff --git a/hugegraph-store/Dockerfile b/hugegraph-store/Dockerfile
index 474fe5984..9e6fbdf18 100644
--- a/hugegraph-store/Dockerfile
+++ b/hugegraph-store/Dockerfile
@@ -35,7 +35,8 @@ RUN --mount=type=cache,id=hugegraph-maven-${SOURCE_REVISION},target=/root/.m2,sh
 
 # 2nd stage: runtime env
 # Note: ZGC (The Z Garbage Collector) is only supported on ARM-Mac with java > 13
-FROM eclipse-temurin:17-jre-jammy
+FROM eclipse-temurin:17-jdk-jammy
+# Local Arthas attach requires the JDK attach and compiler modules.
 
 LABEL maintainer="HugeGraph Docker Maintainers <de...@hugegraph.apache.org>"
 
diff --git a/hugegraph-store/README.md b/hugegraph-store/README.md
index e74239134..3615649ff 100644
--- a/hugegraph-store/README.md
+++ b/hugegraph-store/README.md
@@ -415,6 +415,9 @@ timeout handling, diagnostics and resource drain behavior.
 
 ## Documentation
 
+For offline diagnosis of a running node without restarting it, see
+[Arthas diagnostics](../docs/server-runtime.md#arthas-diagnostics).
+
 See the [storage lifecycle guide](../docs/storage-lifecycle.md) for request ownership,
 shutdown, recovery and provider configuration.
 
diff --git a/hugegraph-store/hg-store-dist/pom.xml b/hugegraph-store/hg-store-dist/pom.xml
index c75fc2ad6..ab53403f3 100644
--- a/hugegraph-store/hg-store-dist/pom.xml
+++ b/hugegraph-store/hg-store-dist/pom.xml
@@ -38,6 +38,12 @@
         <executable.jar.name>hg-store-node</executable.jar.name>
     </properties>
     <dependencies>
+        <dependency>
+            <groupId>com.taobao.arthas</groupId>
+            <artifactId>arthas-packaging</artifactId>
+            <version>${arthas.version}</version>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.hugegraph</groupId>
             <artifactId>hg-store-node</artifactId>
@@ -93,6 +99,33 @@
                 <artifactId>maven-antrun-plugin</artifactId>
                 <version>1.3</version>
                 <executions>
+                    <execution>
+                        <id>prepare-arthas</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>run</goal>
+                        </goals>
+                        <configuration>
+                            <tasks>
+                                <unzip src="${maven.dependency.com.taobao.arthas.arthas-packaging.jar.path}"
+                                       dest="${project.build.directory}/arthas-packaging">
+                                    <patternset>
+                                        <include name="arthas-bin.zip"/>
+                                    </patternset>
+                                </unzip>
+                                <delete dir="${project.build.directory}/arthas"/>
+                                <unzip src="${project.build.directory}/arthas-packaging/arthas-bin.zip"
+                                       dest="${project.build.directory}/arthas">
+                                    <patternset>
+                                        <include name="arthas-*.jar"/>
+                                        <include name="logback.xml"/>
+                                        <include name="async-profiler/**"/>
+                                        <include name="lib/**"/>
+                                    </patternset>
+                                </unzip>
+                            </tasks>
+                        </configuration>
+                    </execution>
                     <execution>
                         <phase>package</phase>
                         <goals>
diff --git a/hugegraph-store/hg-store-dist/src/assembly/descriptor/server-assembly.xml b/hugegraph-store/hg-store-dist/src/assembly/descriptor/server-assembly.xml
index 47170a6c2..ee2b6af9b 100644
--- a/hugegraph-store/hg-store-dist/src/assembly/descriptor/server-assembly.xml
+++ b/hugegraph-store/hg-store-dist/src/assembly/descriptor/server-assembly.xml
@@ -25,7 +25,10 @@
 
     <!--    <format>tar.gz</format>-->
     <fileSets>
-
+        <fileSet>
+            <directory>${project.build.directory}/arthas</directory>
+            <outputDirectory>lib/arthas</outputDirectory>
+        </fileSet>
         <fileSet>
             <directory>${assembly.static.dir}/bin</directory>
             <outputDirectory>bin</outputDirectory>
@@ -47,6 +50,16 @@
     </fileSets>
 
     <files>
+        <file>
+            <source>${project.basedir}/../../hugegraph-server/hugegraph-dist/src/assembly/static/bin/ArthasAttach.java</source>
+            <outputDirectory>bin</outputDirectory>
+            <fileMode>644</fileMode>
+        </file>
+        <file>
+            <source>${project.basedir}/../../hugegraph-server/hugegraph-dist/src/assembly/static/bin/arthas.sh</source>
+            <outputDirectory>bin</outputDirectory>
+            <fileMode>755</fileMode>
+        </file>
         <file>
             <source>${project.basedir}/../../hugegraph-server/hugegraph-dist/src/assembly/static/bin/prepare-topling.sh</source>
             <outputDirectory>bin</outputDirectory>
diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/conf/application.yml b/hugegraph-store/hg-store-dist/src/assembly/static/conf/application.yml
index 8e2b8d4f7..90a5cecbb 100644
--- a/hugegraph-store/hg-store-dist/src/assembly/static/conf/application.yml
+++ b/hugegraph-store/hg-store-dist/src/assembly/static/conf/application.yml
@@ -62,3 +62,6 @@ logging:
   config: 'file:./conf/log4j2.xml'
   level:
     root: info
+
+
+# HTTP Arthas activation is disabled. Use bin/arthas.sh locally without restarting Store.
diff --git a/hugegraph-store/hg-store-node/pom.xml b/hugegraph-store/hg-store-node/pom.xml
index 9c952507a..2a2000850 100644
--- a/hugegraph-store/hg-store-node/pom.xml
+++ b/hugegraph-store/hg-store-node/pom.xml
@@ -121,17 +121,6 @@
             <artifactId>hg-store-core</artifactId>
         </dependency>
 
-        <dependency>
-            <groupId>com.taobao.arthas</groupId>
-            <artifactId>arthas-agent-attach</artifactId>
-            <version>3.6.4</version>
-        </dependency>
-        <dependency>
-            <groupId>com.taobao.arthas</groupId>
-            <artifactId>arthas-packaging</artifactId>
-            <version>3.6.4</version>
-        </dependency>
-
         <dependency>
             <groupId>org.apache.logging.log4j</groupId>
             <artifactId>log4j-core</artifactId>
diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/AppConfig.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/AppConfig.java
index c69f5fe89..0bc57d22d 100644
--- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/AppConfig.java
+++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/AppConfig.java
@@ -214,10 +214,10 @@ public class AppConfig {
         @Value("${arthas.httpPort:8565}")
         private String httpPort;
 
-        @Value("${arthas.ip:0.0.0.0}")
+        @Value("${arthas.ip:127.0.0.1}")
         private String arthasip;
 
-        @Value("${arthas.disabledCommands:jad}")
+        @Value("${arthas.disabledCommands:}")
         private String disCmd;
     }
 
diff --git a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/PartitionAPI.java b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/PartitionAPI.java
index 39f500ad9..59cff949e 100644
--- a/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/PartitionAPI.java
+++ b/hugegraph-store/hg-store-node/src/main/java/org/apache/hugegraph/store/node/controller/PartitionAPI.java
@@ -19,6 +19,7 @@ package org.apache.hugegraph.store.node.controller;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -33,11 +34,12 @@ import org.apache.hugegraph.store.business.BusinessHandler;
 import org.apache.hugegraph.store.business.InnerKeyCreator;
 import org.apache.hugegraph.store.meta.Partition;
 import org.apache.hugegraph.store.metric.HgStoreMetric;
-import org.apache.hugegraph.store.node.AppConfig;
 import org.apache.hugegraph.store.node.grpc.HgStoreNodeService;
 import org.apache.hugegraph.util.Bytes;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -47,7 +49,6 @@ import org.springframework.web.bind.annotation.RestController;
 
 import com.alipay.sofa.jraft.entity.PeerId;
 import com.alipay.sofa.jraft.util.Endpoint;
-import com.taobao.arthas.agent.attach.ArthasAgent;
 
 import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
@@ -60,9 +61,6 @@ public class PartitionAPI {
     @Autowired
     HgStoreNodeService nodeService;
 
-    @Autowired
-    AppConfig appConfig;
-
     @GetMapping(value = "/partitions", produces = "application/json")
     public Map<String, Object> getPartitions(
             @RequestParam(required = false, defaultValue = "") String flags) {
@@ -198,19 +196,11 @@ public class PartitionAPI {
     }
 
     @GetMapping(value = "/arthasstart", produces = "application/json")
-    public Map<String, Object> arthasstart(
-            @RequestParam(required = false, defaultValue = "") String flags) {
-        HashMap<String, String> configMap = new HashMap<>();
-        configMap.put("arthas.telnetPort", appConfig.getArthasConfig().getTelnetPort());
-        configMap.put("arthas.httpPort", appConfig.getArthasConfig().getHttpPort());
-        configMap.put("arthas.ip", appConfig.getArthasConfig().getArthasip());
-        configMap.put("arthas.disabledCommands", appConfig.getArthasConfig().getDisCmd());
-        ArthasAgent.attach(configMap);
-        // TODO: remove this commented-out line, retPose is never used
-//        DashResponse retPose = new DashResponse();
-        List<String> ret = new ArrayList<>();
-        ret.add("Arthas started successfully");
-        return okMap("arthasstart", ret);
+    public ResponseEntity<Map<String, String>> arthasstart() {
+        // Store has no administrator authentication. Reject all HTTP callers,
+        // including loopback proxies, and use OS permissions for local attach.
+        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Collections.singletonMap(
+                "message", "HTTP Arthas activation is disabled; run bin/arthas.sh on the Store host"));
     }
 
     @PostMapping("/compat")
diff --git a/hugegraph-store/hg-store-node/src/main/resources/application.yml b/hugegraph-store/hg-store-node/src/main/resources/application.yml
index 0b6527060..9b70aa2c6 100644
--- a/hugegraph-store/hg-store-node/src/main/resources/application.yml
+++ b/hugegraph-store/hg-store-node/src/main/resources/application.yml
@@ -49,3 +49,6 @@ logging:
   config: classpath:log4j2-dev.xml
   level:
     root: info
+
+
+# HTTP Arthas activation is disabled. Use bin/arthas.sh locally without restarting Store.
diff --git a/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/ArthasSecurityTest.java b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/ArthasSecurityTest.java
new file mode 100644
index 000000000..057ac1f70
--- /dev/null
+++ b/hugegraph-store/hg-store-node/src/test/java/org/apache/hugegraph/store/node/controller/ArthasSecurityTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.hugegraph.store.node.controller;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpHeaders;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+public class ArthasSecurityTest {
+
+    private MockMvc mvc;
+
+    @BeforeEach
+    public void setup() {
+        // No Store engine or Arthas configuration is needed to reject activation.
+        this.mvc = MockMvcBuilders.standaloneSetup(new PartitionAPI()).build();
+    }
+
+    @Test
+    public void testRemoteActivationIsForbidden() throws Exception {
+        assertForbidden(requestFrom("192.0.2.1"));
+    }
+
+    @Test
+    public void testLoopbackActivationIsForbidden() throws Exception {
+        assertForbidden(requestFrom("127.0.0.1"));
+        assertForbidden(requestFrom("::1"));
+    }
+
+    @Test
+    public void testSameHostProxyCannotActivateArthas() throws Exception {
+        assertForbidden(requestFrom("127.0.0.1")
+                        .header("Forwarded", "for=192.0.2.1;host=store.example")
+                        .header("X-Forwarded-For", "192.0.2.1"));
+    }
+
+    @Test
+    public void testSpoofedLoopbackHeadersCannotActivateArthas() throws Exception {
+        assertForbidden(requestFrom("192.0.2.1")
+                        .header("Forwarded", "for=127.0.0.1")
+                        .header("X-Forwarded-For", "127.0.0.1"));
+    }
+
+    @Test
+    public void testAuthorizationHeaderDoesNotEnableStoreActivation() throws Exception {
+        assertForbidden(requestFrom("127.0.0.1")
+                        .header(HttpHeaders.AUTHORIZATION, "Bearer admin"));
+    }
+
+    private static MockHttpServletRequestBuilder requestFrom(String address) {
+        return get("/v1/arthasstart").with(request -> {
+            request.setRemoteAddr(address);
+            return request;
+        });
+    }
+
+    private void assertForbidden(MockHttpServletRequestBuilder request) throws Exception {
+        this.mvc.perform(request)
+                .andExpect(status().isForbidden())
+                .andExpect(jsonPath("$.message").value(
+                        "HTTP Arthas activation is disabled; run bin/arthas.sh on the Store host"));
+    }
+}
diff --git a/install-dist/release-docs/LICENSE b/install-dist/release-docs/LICENSE
index 545b95664..6e27cb1e2 100644
--- a/install-dist/release-docs/LICENSE
+++ b/install-dist/release-docs/LICENSE
@@ -265,6 +265,8 @@ Arthas 4.1.5 includes shaded Jackson, FastDoubleParser, ASM 9.9, Logback 1.2.13
 and RSyntaxTextArea 3.3.4.
 Their additional MIT, BSD, Boost and EPL 1.0 license texts and source locations
 are in licenses/LICENSE-arthas-packaging-4.1.5.txt.
+The same bundle supplies the agent, boot, client, core and spy JARs unpacked
+under lib/arthas for offline local diagnosis.
 Paths referenced by upstream licenses are reproduced as labeled sections in
 the corresponding per-component license file.
 
@@ -363,9 +365,7 @@ the corresponding per-component license file.
     https://central.sonatype.com/artifact/com.squareup.okio/okio-jvm/3.0.0 -> Apache 2.0
     https://central.sonatype.com/artifact/com.squareup.okio/okio/1.15.0 -> Apache 2.0
     https://central.sonatype.com/artifact/com.squareup/javapoet/1.13.0 -> Apache 2.0
-    https://central.sonatype.com/artifact/com.taobao.arthas/arthas-agent-attach/3.6.4 -> Apache 2.0
     https://central.sonatype.com/artifact/com.taobao.arthas/arthas-agent-attach/4.1.5 -> Apache 2.0
-    https://central.sonatype.com/artifact/com.taobao.arthas/arthas-packaging/3.6.4 -> Apache 2.0
     https://central.sonatype.com/artifact/com.taobao.arthas/arthas-packaging/4.1.5 -> Apache 2.0
     https://central.sonatype.com/artifact/com.vaadin.external.google/android-json/0.0.20131108.vaadin1 -> Apache 2.0
     https://central.sonatype.com/artifact/commons-beanutils/commons-beanutils/1.9.4 -> Apache 2.0
@@ -494,7 +494,6 @@ the corresponding per-component license file.
     https://central.sonatype.com/artifact/javax.validation/validation-api/1.1.0.Final -> Apache 2.0
     https://central.sonatype.com/artifact/joda-time/joda-time/2.10.8 -> Apache 2.0
     https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy-agent/1.10.20 -> Apache 2.0
-    https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy-agent/1.11.6 -> Apache 2.0
     https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy-agent/1.18.3 -> Apache 2.0
     https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy-agent/1.17.6 -> Apache 2.0
     https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy/1.10.20 -> Apache 2.0
@@ -696,7 +695,6 @@ the corresponding per-component license file.
     https://central.sonatype.com/artifact/org.springframework/spring-webmvc/5.3.27 -> Apache 2.0
     https://central.sonatype.com/artifact/org.xmlunit/xmlunit-core/2.8.4 -> Apache 2.0
     https://central.sonatype.com/artifact/org.yaml/snakeyaml/2.2 -> Apache 2.0
-    https://central.sonatype.com/artifact/org.zeroturnaround/zt-zip/1.14 -> Apache 2.0
     https://central.sonatype.com/artifact/org.zeroturnaround/zt-zip/1.16 -> Apache 2.0
     https://central.sonatype.com/artifact/org.apache.commons/commons-pool2/2.0 -> Apache 2.0
     https://central.sonatype.com/artifact/org.apache.lucene/lucene-analyzers-common/5.2.1 -> Apache 2.0
diff --git a/install-dist/scripts/dependency/known-dependencies.txt b/install-dist/scripts/dependency/known-dependencies.txt
index 88a0e0a92..17f97dc5b 100644
--- a/install-dist/scripts/dependency/known-dependencies.txt
+++ b/install-dist/scripts/dependency/known-dependencies.txt
@@ -14,10 +14,13 @@ antlr4-runtime-4.9.1.jar
 aopalliance-repackaged-3.0.1.jar
 apiguardian-api-1.1.0.jar
 apiguardian-api-1.1.2.jar
-arthas-agent-attach-3.6.4.jar
 arthas-agent-attach-4.1.5.jar
-arthas-packaging-3.6.4.jar
+arthas-agent.jar
+arthas-boot.jar
+arthas-client.jar
+arthas-core.jar
 arthas-packaging-4.1.5.jar
+arthas-spy.jar
 asm-6.0.jar
 asm-commons-9.4.jar
 asm-tree-9.4.jar
@@ -33,7 +36,6 @@ bolt-1.6.4.jar
 byte-buddy-1.10.20.jar
 byte-buddy-1.17.6.jar
 byte-buddy-agent-1.10.20.jar
-byte-buddy-agent-1.11.6.jar
 byte-buddy-agent-1.17.6.jar
 byte-buddy-agent-1.18.3.jar
 caffeine-2.3.1.jar
@@ -515,5 +517,4 @@ xmlunit-core-2.8.4.jar
 zjsonpatch-0.3.0.jar
 zookeeper-3.8.6.jar
 zookeeper-jute-3.8.6.jar
-zt-zip-1.14.jar
 zt-zip-1.16.jar
diff --git a/pom.xml b/pom.xml
index d5c382037..6e6134b9a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -86,6 +86,7 @@
     </scm>
 
     <properties>
+        <arthas.version>4.1.5</arthas.version>
         <fabric8.version>5.6.0</fabric8.version>
         <revision>1.8.0</revision>
         <groovy.version>4.0.25</groovy.version>
@@ -281,6 +282,7 @@
                             <exclude>**/pd/grpc/**/*.java</exclude>
                             <exclude>**/store/grpc/**/*.java</exclude>
                             <exclude>**/target/**</exclude>
+                            <exclude>**/apache-hugegraph-*/lib/arthas/logback.xml</exclude>
                             <exclude>dist/**/*</exclude>
                             <!-- Git & GitHub  -->
                             <exclude>.github/**/*</exclude>