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

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 200566e6 fix(security): harden SSRF guards, auth fail-closed and 
actuator exposure (#1673)
200566e6 is described below

commit 200566e6a4d8a4874f8e8bdc10a1d1d3b26cff50
Author: zhaohai <[email protected]>
AuthorDate: Tue Aug 11 21:25:16 2026 +0800

    fix(security): harden SSRF guards, auth fail-closed and actuator exposure 
(#1673)
    
    - Add shared UrlHostGuard (common.util) and apply it on every path that
      makes a server-side request to a caller-supplied URL:
      * SettingsService.createDataSource/updateDataSource now reject loopback,
        link-local and metadata addresses when saving (previously only the test
        path was guarded, so a data source pointing at 127.0.0.1 could be stored
        and queried later);
      * AbstractPrometheusCompatibleMetricsSource validates the stored base URL
        on every query (defense in depth against direct DB edits);
      * LlmConfigService.validate() (used by both save and test) now rejects
        link-local/metadata hosts while allowing loopback, so a local ollama
        gateway keeps working.
      The guard fails closed: unresolvable hosts are rejected instead of being
      handed to the connection layer, removing the UnknownHostException bypass.
    - Auth: default studio.auth.login-required=true (application.yml, compose,
      deploy.sh) and AuthInterceptor now fails closed when the settings store
      cannot be read.
    - Deploy: MySQL root password externalized to MYSQL_ROOT_PASSWORD env with a
      compose-default fallback; healthcheck reads the password from the env;
      /actuator/ is no longer proxied wholesale at the edge (only 
/actuator/health
      is exposed) in both web/nginx.conf and deploy/nginx.conf; deploy.sh no
      longer publishes the backend 8888 port to the host.
    
    Tests: SettingsServiceTest (incl. two new SSRF rejection tests), 
LlmConfigServiceTest,
    AuthInterceptorTest all green.
---
 deploy/deploy.sh                                   |   4 +-
 deploy/docker-compose.yml                          |   8 +-
 deploy/nginx.conf                                  |   8 +-
 .../rocketmq/studio/auth/AuthInterceptor.java      |   5 +-
 .../AbstractPrometheusCompatibleMetricsSource.java |   4 +
 .../rocketmq/studio/common/util/UrlHostGuard.java  | 115 +++++++++++++++++++++
 .../rocketmq/studio/ops/ai/LlmConfigService.java   |  11 +-
 .../rocketmq/studio/settings/SettingsService.java  |  16 ++-
 server/src/main/resources/application.yml          |   2 +-
 .../studio/settings/SettingsServiceTest.java       |  38 ++++++-
 web/nginx.conf                                     |   9 +-
 11 files changed, 200 insertions(+), 20 deletions(-)

diff --git a/deploy/deploy.sh b/deploy/deploy.sh
index e7cb96c1..0ceb6469 100755
--- a/deploy/deploy.sh
+++ b/deploy/deploy.sh
@@ -128,14 +128,14 @@ deploy_remote() {
 
   if [[ "$TARGET" == "all" || "$TARGET" == "server" ]]; then
     info "重启 rocketmq-server..."
+    # Backend port is only exposed inside the podman network; the edge nginx 
proxies /api/.
     ssh "$REMOTE" "
       podman rm -f rocketmq-server 2>/dev/null || true
       podman run -d \
         --name rocketmq-server \
         --network $NETWORK \
         --restart unless-stopped \
-        -p 8888:8888 \
-        -e STUDIO_AUTH_LOGIN_REQUIRED=\"${STUDIO_AUTH_LOGIN_REQUIRED:-false}\" 
\
+        -e STUDIO_AUTH_LOGIN_REQUIRED=\"${STUDIO_AUTH_LOGIN_REQUIRED:-true}\" \
         -e STUDIO_AUTH_ADMIN_USERNAME=\"${STUDIO_AUTH_ADMIN_USERNAME:-}\" \
         -e STUDIO_AUTH_ADMIN_PASSWORD=\"${STUDIO_AUTH_ADMIN_PASSWORD:-}\" \
         -e 
STUDIO_METRICS_PROMETHEUS_BASE_URL=\"${STUDIO_METRICS_PROMETHEUS_BASE_URL:-}\" \
diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml
index 9c08c7e0..aeb294e5 100644
--- a/deploy/docker-compose.yml
+++ b/deploy/docker-compose.yml
@@ -7,7 +7,7 @@ services:
     restart: unless-stopped
     environment:
       TZ: ${TZ:-Asia/Shanghai}
-      MYSQL_ROOT_PASSWORD: studio123
+      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-studio123}
       MYSQL_DATABASE: rocketmq
     ports:
       - "3306:3306"
@@ -19,7 +19,7 @@ services:
       - studio-net
       - rocketmq
     healthcheck:
-      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", 
"-pstudio123"]
+      test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot 
-p\"$$MYSQL_ROOT_PASSWORD\""]
       interval: 10s
       timeout: 5s
       retries: 10
@@ -33,7 +33,7 @@ services:
     restart: unless-stopped
     environment:
       TZ: ${TZ:-Asia/Shanghai}
-      STUDIO_AUTH_LOGIN_REQUIRED: ${STUDIO_AUTH_LOGIN_REQUIRED:-false}
+      STUDIO_AUTH_LOGIN_REQUIRED: ${STUDIO_AUTH_LOGIN_REQUIRED:-true}
       STUDIO_AUTH_ADMIN_USERNAME: ${STUDIO_AUTH_ADMIN_USERNAME:-}
       STUDIO_AUTH_ADMIN_PASSWORD: ${STUDIO_AUTH_ADMIN_PASSWORD:-}
       STUDIO_METRICS_PROMETHEUS_BASE_URL: 
${STUDIO_METRICS_PROMETHEUS_BASE_URL:-}
@@ -43,7 +43,7 @@ services:
       SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-prod}
       SPRING_DATASOURCE_URL: 
jdbc:mysql://mysql:3306/rocketmq?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8&connectionCollation=utf8mb4_unicode_ci
       SPRING_DATASOURCE_USERNAME: root
-      SPRING_DATASOURCE_PASSWORD: studio123
+      SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD:-studio123}
       STUDIO_ROCKETMQ_NAMESRV_ADDR: 
${STUDIO_ROCKETMQ_NAMESRV_ADDR:-nameserver:9876}
       RMQ_LLM_TOKEN: ${RMQ_LLM_TOKEN:-}
       RMQ_ANTHROPIC_BASE_URL: ${RMQ_ANTHROPIC_BASE_URL:-}
diff --git a/deploy/nginx.conf b/deploy/nginx.conf
index 0979fe79..9b50e505 100644
--- a/deploy/nginx.conf
+++ b/deploy/nginx.conf
@@ -16,11 +16,17 @@ server {
         proxy_set_header X-Forwarded-Proto $scheme;
     }
 
-    location /actuator/ {
+    # Only the health endpoint is reachable through the edge; the rest of
+    # /actuator/ must never be exposed without authentication.
+    location ~ ^/actuator/health {
         proxy_pass http://rocketmq-server:8888;
         proxy_set_header Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
     }
+
+    location ~ ^/actuator/ {
+        return 404;
+    }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java 
b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
index 9d68f691..685183ff 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
@@ -77,12 +77,13 @@ public class AuthInterceptor implements HandlerInterceptor {
             return true;
         }
         if (settingsRepository == null) {
-            return false;
+            return true;
         }
         try {
             GeneralSettingsVO settings = 
settingsRepository.loadGeneralSettings();
-            return settings != null && settings.isRequireLogin();
+            return settings == null || settings.isRequireLogin();
         } catch (Exception exception) {
+            // Fail closed: when the policy cannot be read, default to 
requiring login.
             return true;
         }
     }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
index 6344b4bc..5b78a2d4 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/AbstractPrometheusCompatibleMetricsSource.java
@@ -178,6 +178,10 @@ public abstract class 
AbstractPrometheusCompatibleMetricsSource implements Metri
             while (baseUrl.endsWith("/")) {
                 baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
             }
+            // SSRF guard: a stored data source is queried server-side on 
every request, so the
+            // host must be validated here even though it was checked when the 
data source was
+            // saved (a pre-save check alone is bypassable via direct DB 
edits).
+            org.apache.rocketmq.studio.common.util.UrlHostGuard.check(baseUrl, 
false);
             URI uri = URI.create(baseUrl + settings.getQueryPath());
             if (!"http".equalsIgnoreCase(uri.getScheme()) && 
!"https".equalsIgnoreCase(uri.getScheme())) {
                 throw new IllegalArgumentException("Unsupported " + 
backendLabel() + " URL scheme");
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/common/util/UrlHostGuard.java 
b/server/src/main/java/org/apache/rocketmq/studio/common/util/UrlHostGuard.java
new file mode 100644
index 00000000..76d68c48
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/common/util/UrlHostGuard.java
@@ -0,0 +1,115 @@
+/*
+ * 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.rocketmq.studio.common.util;
+
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.UnknownHostException;
+import java.util.Locale;
+
+/**
+ * Shared SSRF guard for server-side HTTP endpoints that accept a 
caller-supplied URL
+ * (data-source test/save/query paths and the LLM gateway configuration).
+ *
+ * <p>Rejects hosts that resolve to loopback, link-local (including the cloud 
metadata
+ * range {@code 169.254.169.254}) or any-local addresses. Unlike the old 
per-service
+ * checks, unresolvable hosts are rejected (fail-closed) instead of being 
handed to the
+ * connection attempt, and the same rule is applied on every path — save, test 
and query —
+ * so a host cannot be stored once and queried later.
+ *
+ * <p>Private site-local ranges (10.x, 172.16-31.x, 192.168.x) stay allowed: 
on-premise
+ * Prometheus servers and internal LLM gateways legitimately live on the 
internal network.
+ * When {@code allowLoopback} is set (LLM config, where a local {@code ollama} 
gateway is
+ * a supported provider) loopback is admitted but link-local/metadata 
addresses are still
+ * rejected.
+ */
+public final class UrlHostGuard {
+
+    private UrlHostGuard() {
+    }
+
+    /**
+     * Validates that {@code url} is an http(s) URL whose host passes the SSRF 
guard.
+     *
+     * @param url            the caller-supplied URL
+     * @param allowLoopback  whether loopback hosts ({@code localhost}, 
127.x.x.x, ::1)
+     *                       are acceptable — used for local LLM gateways such 
as ollama
+     * @throws IllegalArgumentException when the URL is missing, non-http(s), 
hostless or
+     *                                  points at a disallowed address
+     */
+    public static void check(String url, boolean allowLoopback) {
+        if (url == null || url.isBlank()) {
+            throw new IllegalArgumentException("URL is required");
+        }
+        String normalized = url.strip();
+        while (normalized.endsWith("/")) {
+            normalized = normalized.substring(0, normalized.length() - 1);
+        }
+        URI uri;
+        try {
+            uri = new URI(normalized);
+        } catch (URISyntaxException exception) {
+            throw new IllegalArgumentException("URL is not a valid URI");
+        }
+        String scheme = uri.getScheme() == null ? "" : 
uri.getScheme().toLowerCase(Locale.ROOT);
+        if (!"http".equals(scheme) && !"https".equals(scheme)) {
+            throw new IllegalArgumentException("URL must start with http:// or 
https://";);
+        }
+        if (uri.getHost() == null || uri.getHost().isBlank()) {
+            throw new IllegalArgumentException("URL must include a host");
+        }
+        if (!isAllowedHost(uri.getHost(), allowLoopback)) {
+            throw new IllegalArgumentException(
+                    "URL must not point to a local, loopback or metadata 
address");
+        }
+    }
+
+    /**
+     * Whether {@code host} is allowed by the SSRF guard.
+     *
+     * @param host           the hostname or IP literal
+     * @param allowLoopback  see {@link #check(String, boolean)}
+     * @return {@code true} when the host is safe to connect to
+     */
+    public static boolean isAllowedHost(String host, boolean allowLoopback) {
+        if (host == null || host.isBlank()) {
+            return false;
+        }
+        String normalized = host.toLowerCase(Locale.ROOT);
+        // Strip a single trailing dot (fully-qualified names like 
"host.example.com.").
+        if (normalized.endsWith(".")) {
+            normalized = normalized.substring(0, normalized.length() - 1);
+        }
+        if ("localhost".equals(normalized)) {
+            return allowLoopback;
+        }
+        try {
+            InetAddress address = InetAddress.getByName(normalized);
+            if (address.isAnyLocalAddress() || address.isLinkLocalAddress()) {
+                return false;
+            }
+            if (address.isLoopbackAddress()) {
+                return allowLoopback;
+            }
+            return true;
+        } catch (UnknownHostException exception) {
+            // Fail closed: an unresolvable host must not be handed to the 
connection layer.
+            return false;
+        }
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmConfigService.java 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmConfigService.java
index fe7543a5..16a35e66 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmConfigService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/LlmConfigService.java
@@ -19,6 +19,7 @@ package org.apache.rocketmq.studio.ops.ai;
 
 import org.springframework.util.StringUtils;
 
+import org.apache.rocketmq.studio.common.util.UrlHostGuard;
 import org.apache.rocketmq.studio.settings.GeneralSettingsVO;
 import org.apache.rocketmq.studio.settings.SettingsService;
 import lombok.RequiredArgsConstructor;
@@ -321,8 +322,14 @@ public class LlmConfigService {
         try {
             URI uri = new URI(apiBase);
             String scheme = uri.getScheme() == null ? "" : 
uri.getScheme().toLowerCase(Locale.ROOT);
-            return ("http".equals(scheme) || "https".equals(scheme)) && 
!!StringUtils.hasText(uri.getHost())
-                    && !apiBase.endsWith(CHAT_COMPLETIONS_PATH);
+            if (!("http".equals(scheme) || "https".equals(scheme)) || 
!StringUtils.hasText(uri.getHost())
+                    || apiBase.endsWith(CHAT_COMPLETIONS_PATH)) {
+                return false;
+            }
+            // SSRF guard on both the save and test paths (validate() runs for 
each). Loopback is
+            // allowed because a local ollama gateway is a supported provider, 
but link-local and
+            // cloud-metadata addresses (169.254.x.x) are rejected.
+            return UrlHostGuard.isAllowedHost(uri.getHost(), true);
         } catch (URISyntaxException exception) {
             return false;
         }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java 
b/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
index 8fa14758..f57f4774 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
@@ -21,6 +21,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.rocketmq.studio.audit.OperationAuditService;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.apache.rocketmq.studio.common.util.UrlHostGuard;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.client.SimpleClientHttpRequestFactory;
@@ -33,11 +37,9 @@ import 
org.springframework.web.client.RestClientResponseException;
 import org.springframework.web.util.UriComponentsBuilder;
 
 import java.io.IOException;
-import java.net.InetAddress;
 import java.net.SocketTimeoutException;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.net.UnknownHostException;
 import java.time.Duration;
 import java.util.Arrays;
 import java.util.List;
@@ -155,6 +157,7 @@ public class SettingsService {
         }
         log.info("Creating data source: {}", dataSource.getName());
         dataSource.setKey(UUID.randomUUID().toString());
+        validateDataSourceUrl(dataSource.getUrl());
         DataSourceVO saved = settingsRepository.saveDataSource(dataSource);
         recordDataSourceAudit("CREATE_DATA_SOURCE", saved);
         return saved;
@@ -168,6 +171,7 @@ public class SettingsService {
         String key = normalizeDataSourceKey(dataSource.getKey());
         dataSource.setKey(key);
         log.info("Updating data source: {}", key);
+        validateDataSourceUrl(dataSource.getUrl());
         if (!settingsRepository.replaceDataSource(dataSource)) {
             throw new BusinessException(404, "Data source not found: " + key);
         }
@@ -175,6 +179,14 @@ public class SettingsService {
         return dataSource;
     }
 
+    private void validateDataSourceUrl(String url) {
+        try {
+            UrlHostGuard.check(url, false);
+        } catch (IllegalArgumentException exception) {
+            throw new BusinessException(400, exception.getMessage());
+        }
+    }
+
 
     public void deleteDataSource(String key) {
         String normalizedKey = normalizeDataSourceKey(key);
diff --git a/server/src/main/resources/application.yml 
b/server/src/main/resources/application.yml
index 9db5ce44..3e6d42d9 100644
--- a/server/src/main/resources/application.yml
+++ b/server/src/main/resources/application.yml
@@ -29,7 +29,7 @@ springdoc:
 
 studio:
   auth:
-    login-required: ${STUDIO_AUTH_LOGIN_REQUIRED:false}
+    login-required: ${STUDIO_AUTH_LOGIN_REQUIRED:true}
     users:
       - username: ${STUDIO_AUTH_ADMIN_USERNAME:}
         password: ${STUDIO_AUTH_ADMIN_PASSWORD:}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
index a821fa29..4d741b40 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
@@ -45,6 +45,7 @@ import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
@@ -224,7 +225,7 @@ class SettingsServiceTest {
     @Test
     void createDataSourceShouldAssignKeyBeforeSaving() {
         DataSourceVO input = DataSourceVO.builder().name("New 
DS").type("rocketmq")
-                .url("new-host:9876").build();
+                .url("http://10.1.2.3";).build();
         when(settingsRepository.saveDataSource(any(DataSourceVO.class)))
                 .thenAnswer(invocation -> invocation.getArgument(0));
 
@@ -239,7 +240,8 @@ class SettingsServiceTest {
 
     @Test
     void createDataSourceShouldReplaceClientProvidedKey() {
-        DataSourceVO input = 
DataSourceVO.builder().key("existing-key").name("New DS").build();
+        DataSourceVO input = 
DataSourceVO.builder().key("existing-key").name("New DS")
+                .url("http://10.1.2.3";).build();
         when(settingsRepository.saveDataSource(any(DataSourceVO.class)))
                 .thenAnswer(invocation -> invocation.getArgument(0));
 
@@ -260,10 +262,36 @@ class SettingsServiceTest {
         verifyNoInteractions(settingsRepository);
     }
 
+    @Test
+    void createDataSourceShouldRejectLoopbackUrl() {
+        DataSourceVO input = DataSourceVO.builder().name("Loopback 
DS").type("rocketmq")
+                .url("http://127.0.0.1:9090";).build();
+
+        assertThatThrownBy(() -> settingsService.createDataSource(input))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("local, loopback or metadata address")
+                .extracting("code")
+                .isEqualTo(400);
+        verify(settingsRepository, never()).saveDataSource(any());
+    }
+
+    @Test
+    void updateDataSourceShouldRejectMetadataUrl() {
+        DataSourceVO input = DataSourceVO.builder().key("ds-1").name("Metadata 
DS").type("rocketmq")
+                .url("http://169.254.169.254/latest/meta-data/";).build();
+
+        assertThatThrownBy(() -> settingsService.updateDataSource(input))
+                .isInstanceOf(BusinessException.class)
+                .hasMessageContaining("local, loopback or metadata address")
+                .extracting("code")
+                .isEqualTo(400);
+        verify(settingsRepository, never()).replaceDataSource(any());
+    }
+
     @Test
     void updateDataSourceShouldDelegateToRepository() {
         DataSourceVO input = DataSourceVO.builder().key("ds-1").name("Updated 
DS").type("rocketmq")
-                .url("updated-host:9876").build();
+                .url("http://10.1.2.3";).build();
         when(settingsRepository.replaceDataSource(input)).thenReturn(true);
 
         DataSourceVO result = settingsService.updateDataSource(input);
@@ -290,7 +318,7 @@ class SettingsServiceTest {
     void updateDataSourceShouldRejectUnknownKey() {
         SettingsService service = new SettingsService(settingsRepository, 
RestClient.builder(), new ObjectMapper(), operationAuditService);
         DataSourceVO input = 
DataSourceVO.builder().key("missing").name("Unexpected DS").type("rocketmq")
-                .url("unexpected-host:9876").build();
+                .url("http://10.1.2.3";).build();
 
         assertThatThrownBy(() -> service.updateDataSource(input))
                 .isInstanceOf(BusinessException.class)
@@ -305,7 +333,7 @@ class SettingsServiceTest {
         SettingsService service = new SettingsService(settingsRepository, 
RestClient.builder(), new ObjectMapper(),
                 operationAuditService);
         DataSourceVO input = DataSourceVO.builder().key(" ").name("Unexpected 
DS").type("rocketmq")
-                .url("unexpected-host:9876").build();
+                .url("http://10.1.2.3";).build();
 
         assertThatThrownBy(() -> service.updateDataSource(input))
                 .isInstanceOf(BusinessException.class)
diff --git a/web/nginx.conf b/web/nginx.conf
index ef47cdfa..57426599 100644
--- a/web/nginx.conf
+++ b/web/nginx.conf
@@ -52,7 +52,10 @@ server {
         proxy_set_header X-Forwarded-Proto $scheme;
     }
 
-    location /actuator/ {
+    # Only the health endpoint is reachable through the edge. The rest of
+    # /actuator/ (env, beans, mappings, metrics, ...) must never be exposed
+    # without authentication.
+    location ~ ^/actuator/health {
         set $backend_actuator http://rocketmq-server:8888;
         proxy_pass $backend_actuator;
         proxy_set_header Host $host;
@@ -60,4 +63,8 @@ server {
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
     }
+
+    location ~ ^/actuator/ {
+        return 404;
+    }
 }

Reply via email to