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;
+ }
}