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

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


The following commit(s) were added to refs/heads/master by this push:
     new cf737db2ee [fix] stop serving the openapi document to anonymous 
callers (#4276)
cf737db2ee is described below

commit cf737db2eebb3028f4445bd757280d74a716f376
Author: Duansg <[email protected]>
AuthorDate: Fri Aug 14 10:44:48 2026 +0800

    [fix] stop serving the openapi document to anonymous callers (#4276)
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../src/test/resources/sureness.yml                |  15 +-
 .../config/AuthorizedSwaggerIndexTransformer.java  | 126 +++++++++++++
 .../hertzbeat/manager/config/SwaggerConfig.java    |  28 +++
 .../AuthorizedSwaggerIndexTransformerTest.java     | 105 +++++++++++
 hertzbeat-manager/src/test/resources/sureness.yml  |  15 +-
 .../src/main/resources/application.yml             |  10 +
 hertzbeat-startup/src/main/resources/sureness.yml  |  15 +-
 .../OpenApiDocumentDisabledByDefaultTest.java      | 142 ++++++++++++++
 .../security/SurenessOpenApiDocRuleTest.java       | 207 +++++++++++++++++++++
 home/docs/start/account-modify.md                  |  37 +++-
 .../current/start/account-modify.md                |  37 +++-
 script/application.yml                             |  10 +
 .../hertzbeat-mysql-iotdb/conf/application.yml     |  10 +
 .../hertzbeat-mysql-iotdb/conf/sureness.yml        |  15 +-
 .../hertzbeat-mysql-tdengine/conf/application.yml  |  10 +
 .../hertzbeat-mysql-tdengine/conf/sureness.yml     |  15 +-
 .../conf/application.yml                           |  10 +
 .../conf/sureness.yml                              |  15 +-
 .../conf/application.yml                           |  10 +
 .../conf/sureness.yml                              |  15 +-
 .../conf/application.yml                           |  10 +
 .../conf/sureness.yml                              |  15 +-
 script/sureness.yml                                |  15 +-
 23 files changed, 825 insertions(+), 62 deletions(-)

diff --git a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml 
b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
index 1763868f1e..dff4e65966 100644
--- a/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
+++ b/hertzbeat-e2e/hertzbeat-log-e2e/src/test/resources/sureness.yml
@@ -16,7 +16,7 @@
 ## -- sureness.yml account source -- ##
 
 # config the resource restful api that need auth protection, base rbac
-# rule: api===method===role 
+# rule: api===method===role
 # eg: /api/v1/source1===get===[admin] means /api/v2/host===post support 
role[admin] access.
 # eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by 
any role.
 resourceRole:
@@ -86,9 +86,16 @@ resourceRole:
   - /api/ingestion/otlp/**===get===[admin,user,guest]
   - /api/logs/**===get===[admin,user,guest]
   - /api/traces/**===get===[admin,user,guest]
+  # The OpenAPI document is a map of every route, parameter and model, so it is
+  # scoped like any other administrative resource instead of being anonymous
+  - /v3/api-docs/**===get===[admin]
+  - /v3/api-docs.yaml===get===[admin]
+  - /v3/api-docs.yaml/**===get===[admin]
+  - /v2/api-docs/**===get===[admin]
+  - /swagger-resources/**===get===[admin]
 
 # config the resource restful api that need bypass auth protection
-# rule: api===method 
+# rule: api===method
 # eg: /api/v1/source3===get means /api/v1/source3===get can be access by 
anyone, no need auth.
 excludedResource:
   - /api/alert/sse/**===*
@@ -121,10 +128,6 @@ excludedResource:
   - /**/*.json===get
   - /**/*.woff===get
   - /**/*.eot===get
-  # swagger ui resource
-  - /swagger-resources/**===get
-  - /v2/api-docs===get
-  - /v3/api-docs===get
   # h2 database
   - /h2-console/**===*
 
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/AuthorizedSwaggerIndexTransformer.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/AuthorizedSwaggerIndexTransformer.java
new file mode 100644
index 0000000000..9b71128609
--- /dev/null
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/AuthorizedSwaggerIndexTransformer.java
@@ -0,0 +1,126 @@
+/*
+ * 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.hertzbeat.manager.config;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springdoc.core.properties.SwaggerUiConfigProperties;
+import org.springdoc.core.properties.SwaggerUiOAuthProperties;
+import org.springdoc.core.providers.ObjectMapperProvider;
+import org.springdoc.webmvc.ui.SwaggerIndexPageTransformer;
+import org.springdoc.webmvc.ui.SwaggerWelcomeCommon;
+import org.springframework.core.io.Resource;
+import org.springframework.web.servlet.resource.ResourceTransformerChain;
+import org.springframework.web.servlet.resource.TransformedResource;
+
+/**
+ * Adds the HertzBeat access token to same-origin requests made by Swagger UI.
+ */
+final class AuthorizedSwaggerIndexTransformer extends 
SwaggerIndexPageTransformer {
+
+    private static final String SWAGGER_INITIALIZER = "swagger-initializer.js";
+
+    private static final String PRESETS_MARKER = "presets: [";
+
+    private static final String INTERCEPTOR_MARKER = "requestInterceptor: 
(request) => {";
+
+    private static final String INTERCEPTOR_RETURN = "return request;";
+
+    private static final String AUTHORIZATION_LOGIC = """
+              const currentUrl = new URL(document.URL);
+              const requestUrl = new URL(request.url, 
document.location.origin);
+              const sameOrigin = currentUrl.protocol === requestUrl.protocol 
&& currentUrl.host === requestUrl.host;
+              const token = window.localStorage.getItem('Authorization');
+              if (sameOrigin && token) {
+                request.headers['Authorization'] = `Bearer ${token}`;
+              }
+            """;
+
+    private static final String AUTHORIZATION_INTERCEPTOR = """
+            requestInterceptor: (request) => {
+            %s
+              return request;
+            },
+            """.formatted(AUTHORIZATION_LOGIC.stripTrailing());
+
+    AuthorizedSwaggerIndexTransformer(SwaggerUiConfigProperties 
swaggerUiConfig,
+                                       SwaggerUiOAuthProperties 
swaggerUiOauthProperties,
+                                       SwaggerWelcomeCommon 
swaggerWelcomeCommon,
+                                       ObjectMapperProvider 
objectMapperProvider) {
+        super(swaggerUiConfig, swaggerUiOauthProperties, swaggerWelcomeCommon, 
objectMapperProvider);
+    }
+
+    @Override
+    public Resource transform(HttpServletRequest request, Resource resource,
+                              ResourceTransformerChain transformerChain) 
throws IOException {
+        final Resource transformed = super.transform(request, resource, 
transformerChain);
+        if (!SWAGGER_INITIALIZER.equals(resource.getFilename())) {
+            return transformed;
+        }
+        final String initializer;
+        try (final var input = transformed.getInputStream()) {
+            initializer = new String(input.readAllBytes(), 
StandardCharsets.UTF_8);
+        }
+        return new TransformedResource(transformed,
+                
addAuthorizationInterceptor(initializer).getBytes(StandardCharsets.UTF_8));
+    }
+
+    /**
+     * Springdoc writes an interceptor of its own when csrf support is turned 
on. Two keys
+     * of the same name would silently drop one of them, so the token is 
appended to the
+     * body already there. It goes in at the end because the last write to a 
header wins,
+     * and springdoc can be configured to write to {@code Authorization} as 
well. The
+     * declarations above are named apart from the ones springdoc emits on 
purpose: the
+     * two bodies share a scope, so a collision would be a syntax error.
+     *
+     * @param initializer the swagger initializer script
+     * @return the script with the token attached to its outgoing requests
+     */
+    static String addAuthorizationInterceptor(String initializer) {
+        final int interceptor = initializer.indexOf(INTERCEPTOR_MARKER);
+        if (interceptor >= 0) {
+            final int returnStatement = initializer.indexOf(INTERCEPTOR_RETURN,
+                    interceptor + INTERCEPTOR_MARKER.length());
+            if (returnStatement < 0) {
+                throw new IllegalStateException("the swagger initializer 
interceptor no longer returns the request");
+            }
+            return insertLineBefore(initializer, returnStatement, 
AUTHORIZATION_LOGIC);
+        }
+        final int presets = initializer.indexOf(PRESETS_MARKER);
+        if (presets < 0) {
+            throw new IllegalStateException("the swagger initializer no longer 
contains the presets marker");
+        }
+        return insertLineBefore(initializer, presets, 
AUTHORIZATION_INTERCEPTOR);
+    }
+
+    /**
+     * @param initializer the swagger initializer script
+     * @param index       an index into the line to insert in front of
+     * @param insertion   the lines to insert, newline terminated
+     * @return the script with the insertion on its own lines, leaving the 
indentation of
+     *         the line at {@code index} alone
+     */
+    private static String insertLineBefore(String initializer, int index, 
String insertion) {
+        final int lineStart = initializer.lastIndexOf('\n', index) + 1;
+        return initializer.substring(0, lineStart) + insertion + 
initializer.substring(lineStart);
+    }
+}
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SwaggerConfig.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SwaggerConfig.java
index 295cbc91de..2ea060bf59 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SwaggerConfig.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/config/SwaggerConfig.java
@@ -25,6 +25,13 @@ import io.swagger.v3.oas.models.info.Info;
 import io.swagger.v3.oas.models.info.License;
 import io.swagger.v3.oas.models.security.SecurityRequirement;
 import io.swagger.v3.oas.models.security.SecurityScheme;
+import org.springdoc.core.properties.SwaggerUiConfigProperties;
+import org.springdoc.core.properties.SwaggerUiOAuthProperties;
+import org.springdoc.core.providers.ObjectMapperProvider;
+import org.springdoc.webmvc.ui.SwaggerIndexTransformer;
+import org.springdoc.webmvc.ui.SwaggerWelcomeCommon;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import 
org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
@@ -37,6 +44,27 @@ public class SwaggerConfig {
 
     private static final String SECURITY_SCHEME_NAME = "BearerAuth";
 
+    /**
+     * The springdoc beans this one is built from only exist while both 
switches are on:
+     * its own ui configuration is conditional on {@code 
SpringDocConfiguration}, which
+     * {@code springdoc.api-docs.enabled} gates in turn. Matching both 
switches keeps a
+     * deployment that turns the document off from failing to start.
+     *
+     * @return the swagger ui index transformer, replacing the springdoc 
default
+     */
+    @Bean
+    @ConditionalOnWebApplication(type = 
ConditionalOnWebApplication.Type.SERVLET)
+    @ConditionalOnProperty(name = {"springdoc.api-docs.enabled", 
"springdoc.swagger-ui.enabled"},
+            havingValue = "true", matchIfMissing = true)
+    public SwaggerIndexTransformer authorizedSwaggerIndexTransformer(
+            SwaggerUiConfigProperties swaggerUiConfig,
+            SwaggerUiOAuthProperties swaggerUiOauthProperties,
+            SwaggerWelcomeCommon swaggerWelcomeCommon,
+            ObjectMapperProvider objectMapperProvider) {
+        return new AuthorizedSwaggerIndexTransformer(swaggerUiConfig, 
swaggerUiOauthProperties,
+                swaggerWelcomeCommon, objectMapperProvider);
+    }
+
     @Bean
     public OpenAPI springOpenApi() {
         return new OpenAPI()
diff --git 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/AuthorizedSwaggerIndexTransformerTest.java
 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/AuthorizedSwaggerIndexTransformerTest.java
new file mode 100644
index 0000000000..985e740788
--- /dev/null
+++ 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/config/AuthorizedSwaggerIndexTransformerTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.hertzbeat.manager.config;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test case for {@link AuthorizedSwaggerIndexTransformer}.
+ */
+class AuthorizedSwaggerIndexTransformerTest {
+
+    @Test
+    void shouldAttachTheHertzBeatTokenToSameOriginRequests() {
+        final String initializer = """
+                window.ui = SwaggerUIBundle({
+                  configUrl: "/v3/api-docs/swagger-config",
+                  presets: [SwaggerUIBundle.presets.apis]
+                });
+                """;
+
+        final String transformed = 
AuthorizedSwaggerIndexTransformer.addAuthorizationInterceptor(initializer);
+
+        
assertTrue(transformed.contains("window.localStorage.getItem('Authorization')"));
+        assertTrue(transformed.contains("request.headers['Authorization'] = 
`Bearer ${token}`"));
+        assertTrue(transformed.contains("sameOrigin && token"));
+        assertTrue(transformed.contains("presets: 
[SwaggerUIBundle.presets.apis]"));
+    }
+
+    @Test
+    void shouldFailClosedWhenTheSwaggerInitializerShapeChanges() {
+        assertThrows(IllegalStateException.class,
+                () -> 
AuthorizedSwaggerIndexTransformer.addAuthorizationInterceptor("window.ui = 
{};"));
+    }
+
+    /**
+     * Springdoc writes an interceptor of its own when csrf support is turned 
on, and it
+     * can be configured to write to the {@code Authorization} header too. The 
last write
+     * wins, so ours has to come after the one already there.
+     */
+    @Test
+    void shouldComposeWithAnExistingRequestInterceptor() {
+        final String initializer = """
+                window.ui = SwaggerUIBundle({
+                  requestInterceptor: (request) => {
+                    request.headers['Authorization'] = 'csrf';
+                    return request;
+                  },
+                  presets: [SwaggerUIBundle.presets.apis]
+                });
+                """;
+
+        final String transformed = 
AuthorizedSwaggerIndexTransformer.addAuthorizationInterceptor(initializer);
+
+        assertTrue(transformed.contains("request.headers['Authorization'] = 
'csrf'"));
+        assertTrue(transformed.indexOf("request.headers['Authorization'] = 
`Bearer ${token}`")
+                        > 
transformed.indexOf("request.headers['Authorization'] = 'csrf'"),
+                "the token has to be written after the interceptor that was 
already there");
+        assertEquals(1, countOf(transformed, "requestInterceptor:"),
+                "a second key of the same name would drop one of the two 
interceptors");
+    }
+
+    @Test
+    void shouldFailClosedWhenTheExistingInterceptorDoesNotReturnTheRequest() {
+        final String initializer = """
+                window.ui = SwaggerUIBundle({
+                  requestInterceptor: (request) => {
+                  presets: [SwaggerUIBundle.presets.apis]
+                """;
+
+        assertThrows(IllegalStateException.class,
+                () -> 
AuthorizedSwaggerIndexTransformer.addAuthorizationInterceptor(initializer));
+    }
+
+    /**
+     * @param initializer the transformed script
+     * @param token       the substring to count
+     * @return how many times the substring occurs
+     */
+    private static int countOf(String initializer, String token) {
+        int count = 0;
+        for (int index = initializer.indexOf(token); index >= 0; index = 
initializer.indexOf(token, index + 1)) {
+            count++;
+        }
+        return count;
+    }
+}
diff --git a/hertzbeat-manager/src/test/resources/sureness.yml 
b/hertzbeat-manager/src/test/resources/sureness.yml
index 1763868f1e..dff4e65966 100644
--- a/hertzbeat-manager/src/test/resources/sureness.yml
+++ b/hertzbeat-manager/src/test/resources/sureness.yml
@@ -16,7 +16,7 @@
 ## -- sureness.yml account source -- ##
 
 # config the resource restful api that need auth protection, base rbac
-# rule: api===method===role 
+# rule: api===method===role
 # eg: /api/v1/source1===get===[admin] means /api/v2/host===post support 
role[admin] access.
 # eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by 
any role.
 resourceRole:
@@ -86,9 +86,16 @@ resourceRole:
   - /api/ingestion/otlp/**===get===[admin,user,guest]
   - /api/logs/**===get===[admin,user,guest]
   - /api/traces/**===get===[admin,user,guest]
+  # The OpenAPI document is a map of every route, parameter and model, so it is
+  # scoped like any other administrative resource instead of being anonymous
+  - /v3/api-docs/**===get===[admin]
+  - /v3/api-docs.yaml===get===[admin]
+  - /v3/api-docs.yaml/**===get===[admin]
+  - /v2/api-docs/**===get===[admin]
+  - /swagger-resources/**===get===[admin]
 
 # config the resource restful api that need bypass auth protection
-# rule: api===method 
+# rule: api===method
 # eg: /api/v1/source3===get means /api/v1/source3===get can be access by 
anyone, no need auth.
 excludedResource:
   - /api/alert/sse/**===*
@@ -121,10 +128,6 @@ excludedResource:
   - /**/*.json===get
   - /**/*.woff===get
   - /**/*.eot===get
-  # swagger ui resource
-  - /swagger-resources/**===get
-  - /v2/api-docs===get
-  - /v3/api-docs===get
   # h2 database
   - /h2-console/**===*
 
diff --git a/hertzbeat-startup/src/main/resources/application.yml 
b/hertzbeat-startup/src/main/resources/application.yml
index fa6cda6e10..425dd4e1f6 100644
--- a/hertzbeat-startup/src/main/resources/application.yml
+++ b/hertzbeat-startup/src/main/resources/application.yml
@@ -71,6 +71,16 @@ management:
       export:
         enabled: true
 
+# The generated OpenAPI document is a map of every route, HTTP method, 
parameter
+# and model, so it is not served by default. A deployment that wants the 
Swagger
+# UI opts in by turning both switches on; the document endpoints stay scoped to
+# the admin role in sureness.yml either way.
+springdoc:
+  api-docs:
+    enabled: false
+  swagger-ui:
+    enabled: false
+
 sureness:
   container: jakarta_servlet
   auths:
diff --git a/hertzbeat-startup/src/main/resources/sureness.yml 
b/hertzbeat-startup/src/main/resources/sureness.yml
index f63bee0e8f..8cdaeec049 100644
--- a/hertzbeat-startup/src/main/resources/sureness.yml
+++ b/hertzbeat-startup/src/main/resources/sureness.yml
@@ -16,7 +16,7 @@
 ## -- sureness.yml account source -- ##
 
 # config the resource restful api that need auth protection, base rbac
-# rule: api===method===role 
+# rule: api===method===role
 # eg: /api/v1/source1===get===[admin] means /api/v2/host===post support 
role[admin] access.
 # eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by 
any role.
 resourceRole:
@@ -116,11 +116,18 @@ resourceRole:
   - /api/account/token===get===[admin]
   - /api/account/token/**===post===[admin]
   - /api/account/token/**===delete===[admin]
+  # The OpenAPI document is a map of every route, parameter and model, so it is
+  # scoped like any other administrative resource instead of being anonymous
+  - /v3/api-docs/**===get===[admin]
+  - /v3/api-docs.yaml===get===[admin]
+  - /v3/api-docs.yaml/**===get===[admin]
+  - /v2/api-docs/**===get===[admin]
+  - /swagger-resources/**===get===[admin]
   # spring boot actuator exposes jvm, http and datasource internals for 
scraping
   - /actuator/**===get===[admin]
 
 # config the resource restful api that need bypass auth protection
-# rule: api===method 
+# rule: api===method
 # eg: /api/v1/source3===get means /api/v1/source3===get can be access by 
anyone, no need auth.
 excludedResource:
   - /api/alert/sse/**===*
@@ -157,10 +164,6 @@ excludedResource:
   - /**/*.json===get
   - /**/*.woff===get
   - /**/*.eot===get
-  # swagger ui resource
-  - /swagger-resources/**===get
-  - /v2/api-docs===get
-  - /v3/api-docs===get
   # h2 database
   - /h2-console/**===*
 
diff --git 
a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/OpenApiDocumentDisabledByDefaultTest.java
 
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/OpenApiDocumentDisabledByDefaultTest.java
new file mode 100644
index 0000000000..33af10aeff
--- /dev/null
+++ 
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/OpenApiDocumentDisabledByDefaultTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.hertzbeat.startup.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.yaml.snakeyaml.Yaml;
+
+/**
+ * Guards the springdoc switches that keep the openapi document off by default.
+ *
+ * <p>Scoping the document to the admin role is only half the story. The 
document is a map
+ * of every route, http method, parameter and model, so a deployment that has 
no use for it
+ * should not serve it at all: both switches are off unless a deployment opts 
in, and the
+ * rbac rules stay as the second line of defence for deployments that do. The 
swagger ui
+ * page itself is reachable anonymously through the {@code /**}{@code 
/*.html===get}
+ * exclusion, but it renders nothing until the caller proves it holds the 
admin role.
+ */
+class OpenApiDocumentDisabledByDefaultTest {
+
+    private static final Path SCRIPT_DIR = Path.of("..", "script");
+
+    private static final List<List<String>> SWITCHES = List.of(
+            List.of("springdoc", "api-docs", "enabled"),
+            List.of("springdoc", "swagger-ui", "enabled"));
+
+    @Test
+    void shouldDisableTheOpenApiDocumentInThePackagedConfig() throws 
IOException {
+        try (InputStream in = 
OpenApiDocumentDisabledByDefaultTest.class.getResourceAsStream("/application.yml"))
 {
+            assertNotNull(in, "application.yml must be on the classpath");
+            assertDisabled(documentsOf(in), "application.yml");
+        }
+    }
+
+    /**
+     * The deployment scripts ship their own copies of {@code application.yml} 
and mount
+     * them over the packaged one, so a switch flipped only in the packaged 
file would
+     * still leave every container deployment serving the document.
+     */
+    @Test
+    void shouldDisableTheOpenApiDocumentInDeploymentCopies() throws 
IOException {
+        for (Path copy : deploymentCopies()) {
+            try (InputStream in = Files.newInputStream(copy)) {
+                assertDisabled(documentsOf(in), copy.toString());
+            }
+        }
+    }
+
+    /**
+     * Asserts that every declaration of the springdoc switches across a multi 
document
+     * yaml turns the endpoint off, and that at least one declaration exists - 
a file that
+     * simply omits them falls back to the springdoc default, which is enabled.
+     *
+     * @param documents the yaml documents, in the order spring applies them
+     * @param source    the file the documents came from, for the failure 
message
+     */
+    private static void assertDisabled(List<Map<String, Object>> documents, 
String source) {
+        for (List<String> path : SWITCHES) {
+            List<Object> declarations = documents.stream()
+                    .map(document -> valueAt(document, path))
+                    .filter(Objects::nonNull)
+                    .toList();
+            assertFalse(declarations.isEmpty(),
+                    String.join(".", path) + " is unset in " + source + ", so 
it falls back to enabled");
+            declarations.forEach(declared -> assertEquals(Boolean.FALSE, 
declared,
+                    String.join(".", path) + " is enabled in " + source));
+        }
+    }
+
+    /**
+     * @param document the parsed yaml document
+     * @param path     the key path to walk, outermost first
+     * @return the value at that path, or null when any segment is missing
+     */
+    @Nullable
+    private static Object valueAt(Map<String, Object> document, List<String> 
path) {
+        Object current = document;
+        for (String key : path) {
+            if (!(current instanceof Map<?, ?> map)) {
+                return null;
+            }
+            current = map.get(key);
+        }
+        return current;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static List<Map<String, Object>> documentsOf(InputStream in) {
+        List<Map<String, Object>> documents = new ArrayList<>();
+        for (Object document : new Yaml().loadAll(in)) {
+            if (document instanceof Map<?, ?> map) {
+                documents.add((Map<String, Object>) map);
+            }
+        }
+        return documents;
+    }
+
+    /**
+     * @return the {@code application.yml} copies shipped by the deployment 
scripts
+     */
+    private static Set<Path> deploymentCopies() throws IOException {
+        assumeTrue(Files.isDirectory(SCRIPT_DIR),
+                "running outside the source tree, the packaged file asserted 
above is all we can see");
+        try (Stream<Path> paths = Files.walk(SCRIPT_DIR)) {
+            Set<Path> copies = paths.filter(path -> 
path.getFileName().toString().equals("application.yml"))
+                    .collect(Collectors.toCollection(LinkedHashSet::new));
+            assertFalse(copies.isEmpty(), "expected the deployment scripts to 
ship application.yml copies");
+            return copies;
+        }
+    }
+}
diff --git 
a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessOpenApiDocRuleTest.java
 
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessOpenApiDocRuleTest.java
new file mode 100644
index 0000000000..76ff76b2e3
--- /dev/null
+++ 
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/security/SurenessOpenApiDocRuleTest.java
@@ -0,0 +1,207 @@
+/*
+ * 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.hertzbeat.startup.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import com.usthe.sureness.matcher.util.TirePathTree;
+import jakarta.annotation.Nullable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.yaml.snakeyaml.Yaml;
+
+/**
+ * Guards the rbac rules covering the generated openapi document.
+ *
+ * <p>These paths used to sit in {@code excludedResource}. Sureness checks the 
exclusion
+ * tree before any credential check, so an anonymous request returned the full 
document:
+ * every route, http method, parameter name and type, and every request and 
response
+ * model. That is a ready made map of the attack surface, so it is scoped like 
any other
+ * administrative resource.
+ */
+class SurenessOpenApiDocRuleTest {
+
+    private static final String SEPARATOR = "===";
+
+    /**
+     * {@code excludedResource} entries are written as {@code api===method}, 
but
+     * {@code TirePathTree} only accepts the three segment {@code 
api===method===roles}
+     * shape and silently drops anything else. {@code DefaultPathRoleMatcher} 
therefore
+     * appends this marker to every excluded rule before it builds the 
exclusion tree, and
+     * a test that skips the same step builds an empty tree that matches 
nothing - every
+     * {@code assertNull} against it then passes no matter what the yaml says.
+     */
+    private static final String EXCLUDE_ROLE = SEPARATOR + "[exclude]";
+
+    private static final Path SCRIPT_DIR = Path.of("..", "script");
+
+    private static TirePathTree roleTree;
+
+    private static TirePathTree excludeTree;
+
+    @BeforeAll
+    @SuppressWarnings("unchecked")
+    static void loadSurenessConfig() throws IOException {
+        List<String> resourceRole;
+        List<String> excludedResource;
+        try (InputStream in = 
SurenessOpenApiDocRuleTest.class.getResourceAsStream("/sureness.yml")) {
+            assertNotNull(in, "sureness.yml must be on the classpath");
+            Map<String, Object> document = new Yaml().load(in);
+            resourceRole = (List<String>) document.get("resourceRole");
+            excludedResource = (List<String>) document.get("excludedResource");
+        }
+        assertNotNull(resourceRole, "resourceRole must be present");
+        assertNotNull(excludedResource, "excludedResource must be present");
+        roleTree = new TirePathTree();
+        roleTree.buildTree(new LinkedHashSet<>(resourceRole));
+        excludeTree = excludeTreeOf(excludedResource);
+    }
+
+    @Test
+    void shouldRestrictTheOpenApiDocumentToAdmin() {
+        assertEquals("[admin]", rolesFor(roleTree, "/v3/api-docs"));
+        assertEquals("[admin]", rolesFor(roleTree, "/v2/api-docs"));
+        assertEquals("[admin]", rolesFor(roleTree, 
"/swagger-resources/configuration/ui"));
+    }
+
+    /**
+     * Springdoc also serves the grouped documents and its own config under 
the same
+     * prefix; a rule bound to the bare path would leave those anonymous.
+     */
+    @Test
+    void shouldCoverTheGroupedDocumentsToo() {
+        assertEquals("[admin]", rolesFor(roleTree, 
"/v3/api-docs/swagger-config"));
+        assertEquals("[admin]", rolesFor(roleTree, "/v3/api-docs/default"));
+        assertEquals("[admin]", rolesFor(roleTree, 
"/v3/api-docs.yaml/default"));
+    }
+
+    /**
+     * {@code OpenApiWebMvcResource} maps the document twice, at the 
configured path and at
+     * that path with a {@code .yaml} suffix. The suffixed form is a sibling 
path segment
+     * rather than a child, so {@code /v3/api-docs/**} does not reach it and 
it needs its
+     * own rule. Without one the yaml document carries no role requirement at 
all, which
+     * sureness treats as no restriction for any authenticated caller 
including guest.
+     */
+    @Test
+    void shouldRestrictTheYamlDocumentToAdmin() {
+        assertEquals("[admin]", rolesFor(roleTree, "/v3/api-docs.yaml"));
+    }
+
+    @Test
+    void shouldStopTreatingTheOpenApiDocumentAsAnonymous() {
+        assertNull(rolesFor(excludeTree, "/v3/api-docs"));
+        assertNull(rolesFor(excludeTree, "/v3/api-docs.yaml"));
+        assertNull(rolesFor(excludeTree, "/v3/api-docs.yaml/default"));
+        assertNull(rolesFor(excludeTree, "/v3/api-docs/swagger-config"));
+        assertNull(rolesFor(excludeTree, "/v2/api-docs"));
+        assertNull(rolesFor(excludeTree, 
"/swagger-resources/configuration/ui"));
+    }
+
+    /**
+     * The assertions above are all {@code assertNull}, so they only mean 
something while
+     * the exclusion tree is capable of matching at all. This pins a path that 
is meant to
+     * stay anonymous and fails if the tree was built in a shape sureness 
would have
+     * rejected.
+     */
+    @Test
+    void shouldKeepMatchingIntentionallyExcludedResources() {
+        assertEquals("[exclude]", rolesFor(excludeTree, "/api/i18n/lang"));
+    }
+
+    /**
+     * The deployment scripts ship their own copies of {@code sureness.yml}; a 
rule fixed
+     * only in the packaged file would still leave every container deployment 
serving the
+     * document to anonymous callers.
+     */
+    @Test
+    void deploymentCopiesRestrictTheOpenApiDocument() throws IOException {
+        for (Path copy : deploymentCopies()) {
+            TirePathTree copyRoleTree = new TirePathTree();
+            copyRoleTree.buildTree(new LinkedHashSet<>(sectionOf(copy, 
"resourceRole")));
+            assertEquals("[admin]", rolesFor(copyRoleTree, "/v3/api-docs"),
+                    "the openapi document is unruled or over-granted in " + 
copy);
+            assertEquals("[admin]", rolesFor(copyRoleTree, 
"/v3/api-docs.yaml"),
+                    "the yaml openapi document is unruled or over-granted in " 
+ copy);
+            assertEquals("[admin]", rolesFor(copyRoleTree, 
"/v3/api-docs.yaml/default"),
+                    "the grouped yaml openapi document is unruled or 
over-granted in " + copy);
+            assertEquals("[admin]", rolesFor(copyRoleTree, 
"/v3/api-docs/swagger-config"),
+                    "the springdoc ui config is unruled or over-granted in " + 
copy);
+
+            TirePathTree copyExcludeTree = excludeTreeOf(sectionOf(copy, 
"excludedResource"));
+            assertNull(rolesFor(copyExcludeTree, "/v3/api-docs"),
+                    "the openapi document is still anonymous in " + copy);
+            assertEquals("[exclude]", rolesFor(copyExcludeTree, 
"/api/i18n/lang"),
+                    "the exclusion tree matches nothing in " + copy + ", the 
assertion above proves nothing");
+        }
+    }
+
+    @Nullable
+    private static String rolesFor(TirePathTree tree, String path) {
+        return tree.searchPathFilterRoles(path + SEPARATOR + "get");
+    }
+
+    /**
+     * @param excludedResource the {@code excludedResource} rules, as written 
in the yaml
+     * @return the exclusion tree, built the way {@code 
DefaultPathRoleMatcher} builds it
+     */
+    private static TirePathTree excludeTreeOf(List<String> excludedResource) {
+        TirePathTree tree = new TirePathTree();
+        tree.buildTree(excludedResource.stream()
+                .map(rule -> rule + EXCLUDE_ROLE)
+                .collect(Collectors.toCollection(LinkedHashSet::new)));
+        return tree;
+    }
+
+    /**
+     * @return the {@code sureness.yml} copies shipped by the deployment 
scripts
+     */
+    private static Set<Path> deploymentCopies() throws IOException {
+        assumeTrue(Files.isDirectory(SCRIPT_DIR),
+                "running outside the source tree, the packaged file asserted 
above is all we can see");
+        try (Stream<Path> paths = Files.walk(SCRIPT_DIR)) {
+            Set<Path> copies = paths.filter(path -> 
path.getFileName().toString().equals("sureness.yml"))
+                    .collect(Collectors.toCollection(LinkedHashSet::new));
+            assertFalse(copies.isEmpty(), "expected the deployment scripts to 
ship sureness.yml copies");
+            return copies;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private static List<String> sectionOf(Path copy, String section) throws 
IOException {
+        Map<String, Object> document;
+        try (InputStream in = Files.newInputStream(copy)) {
+            document = new Yaml().load(in);
+        }
+        List<String> rules = (List<String>) document.get(section);
+        assertNotNull(rules, section + " must be present in " + copy);
+        return rules;
+    }
+}
diff --git a/home/docs/start/account-modify.md 
b/home/docs/start/account-modify.md
index ea72ca06a1..8fdba1e21d 100644
--- a/home/docs/start/account-modify.md
+++ b/home/docs/start/account-modify.md
@@ -51,6 +51,13 @@ resourceRole:
   - /api/status/page/**===post===[admin,user]
   - /api/status/page/**===put===[admin,user]
   - /api/status/page/**===delete===[admin]
+  # The OpenAPI document is a map of every route, parameter and model, so it is
+  # scoped like any other administrative resource instead of being anonymous
+  - /v3/api-docs/**===get===[admin]
+  - /v3/api-docs.yaml===get===[admin]
+  - /v3/api-docs.yaml/**===get===[admin]
+  - /v2/api-docs/**===get===[admin]
+  - /swagger-resources/**===get===[admin]
 
 # config the resource restful api that need bypass auth protection
 # rule: api===method 
@@ -82,10 +89,6 @@ excludedResource:
   - /**/*.json===get
   - /**/*.woff===get
   - /**/*.eot===get
-  # swagger ui resource
-  - /swagger-resources/**===get
-  - /v2/api-docs===get
-  - /v3/api-docs===get
   # h2 database
   - /h2-console/**===*
 
@@ -140,6 +143,32 @@ account:
     role: [user]
 ```
 
+## OpenAPI Document And Swagger UI
+
+The generated OpenAPI document lists every route, http method, parameter name 
and type, and every request and response model. It is a ready made map of the 
attack surface, so HertzBeat does not serve it by default: 
`springdoc.api-docs.enabled` and `springdoc.swagger-ui.enabled` are both 
`false` in the shipped `application.yml`, which makes `/v3/api-docs` and 
`/swagger-ui/index.html` return 404.
+
+If you need the document, opt in by updating the `application.yml` file in the 
`config` directory:
+
+```yaml
+springdoc:
+  api-docs:
+    enabled: true
+  swagger-ui:
+    enabled: true
+```
+
+Once enabled, the document endpoints are still scoped to the `admin` role by 
the `resourceRole` rules above. Sign in to the HertzBeat web application as an 
administrator before opening `/swagger-ui/index.html`; the Swagger UI attaches 
the stored HertzBeat token to its same-origin document and try-it-out requests, 
and the page loads without asking for anything.
+
+Without that session the document is not exposed, but the page does not fail 
silently either. `/swagger-ui/index.html` is a static file and still loads; its 
request for `/v3/api-docs/swagger-config` is answered with `401` and a 
`WWW-Authenticate: Digest` challenge, so the browser asks for a username and 
password. Administrator credentials entered there let the document through, and 
an account without the `admin` role is answered with `403`.
+
+The document can also be fetched directly with an administrator token:
+
+```shell
+curl -H "Authorization: Bearer $YOUR_ADMIN_TOKEN" 
http://localhost:1157/v3/api-docs
+```
+
+> ⚠️ Do not move the OpenAPI paths into `excludedResource`; doing so makes the 
complete document anonymous again.
+
 ## Update Security Secret
 
 > This secret is the key for account security encryption management and needs 
 > to be updated to your custom key string of the same length.
diff --git 
a/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/start/account-modify.md
 
b/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/start/account-modify.md
index 9818ff8620..24f82b02e7 100644
--- 
a/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/start/account-modify.md
+++ 
b/home/i18n/zh-cn/docusaurus-plugin-content-docs/current/start/account-modify.md
@@ -52,6 +52,13 @@ resourceRole:
   - /api/status/page/**===post===[admin,user]
   - /api/status/page/**===put===[admin,user]
   - /api/status/page/**===delete===[admin]
+  # OpenAPI 文档包含全部路由、参数与数据模型,等同于一份接口地图,
+  # 因此按普通管理类资源收敛到 admin,不再匿名开放
+  - /v3/api-docs/**===get===[admin]
+  - /v3/api-docs.yaml===get===[admin]
+  - /v3/api-docs.yaml/**===get===[admin]
+  - /v2/api-docs/**===get===[admin]
+  - /swagger-resources/**===get===[admin]
 
 # 需要被过滤保护的资源,不认证鉴权直接访问
 # /api/v1/source3===get 表示 /api/v1/source3===get 可以被任何人访问 无需登录认证鉴权
@@ -82,10 +89,6 @@ excludedResource:
   - /**/*.json===get
   - /**/*.woff===get
   - /**/*.eot===get
-  # swagger ui resource
-  - /swagger-resources/**===get
-  - /v2/api-docs===get
-  - /v3/api-docs===get
   # h2 database
   - /h2-console/**===*
 
@@ -141,6 +144,32 @@ account:
     role: [user]
 ```
 
+## OpenAPI 文档与 Swagger UI
+
+生成的 OpenAPI 文档会列出全部路由、HTTP 方法、参数名与类型,以及所有请求和响应模型,等同于一份现成的攻击面地图,因此 HertzBeat 
默认不对外提供:随包发布的 `application.yml` 中 `springdoc.api-docs.enabled` 与 
`springdoc.swagger-ui.enabled` 均为 `false`,此时 `/v3/api-docs` 和 
`/swagger-ui/index.html` 返回 404。
+
+如果确实需要该文档,更新 `config` 目录下的 `application.yml` 文件显式开启:
+
+```yaml
+springdoc:
+  api-docs:
+    enabled: true
+  swagger-ui:
+    enabled: true
+```
+
+开启之后,文档接口仍然被上面的 `resourceRole` 规则收敛在 `admin` 角色。请先以管理员身份登录 HertzBeat Web 
应用,再打开 `/swagger-ui/index.html`;Swagger UI 会在同源的文档请求与 try-it-out 请求中附带 
HertzBeat 保存的令牌,页面不会再要求任何输入。
+
+没有该会话时文档同样不会泄露,但页面并非静默失败:`/swagger-ui/index.html` 是静态文件,仍然可以打开,而它请求 
`/v3/api-docs/swagger-config` 会得到 `401` 与 `WWW-Authenticate: Digest` 
挑战,浏览器因此弹出用户名密码框。在该弹框中输入管理员账号可以正常加载文档;没有 `admin` 角色的账号则会收到 `403`。
+
+也可以直接携带管理员令牌获取文档:
+
+```shell
+curl -H "Authorization: Bearer $YOUR_ADMIN_TOKEN" 
http://localhost:1157/v3/api-docs
+```
+
+> ⚠️ 不要把 OpenAPI 路径放回 `excludedResource`,否则完整接口文档会再次匿名开放。
+
 ## 更新安全密钥
 
 > 此密钥为账户安全加密管理的密钥,需要更新为相同长度的你自定义密钥串。
diff --git a/script/application.yml b/script/application.yml
index fa6cda6e10..425dd4e1f6 100644
--- a/script/application.yml
+++ b/script/application.yml
@@ -71,6 +71,16 @@ management:
       export:
         enabled: true
 
+# The generated OpenAPI document is a map of every route, HTTP method, 
parameter
+# and model, so it is not served by default. A deployment that wants the 
Swagger
+# UI opts in by turning both switches on; the document endpoints stay scoped to
+# the admin role in sureness.yml either way.
+springdoc:
+  api-docs:
+    enabled: false
+  swagger-ui:
+    enabled: false
+
 sureness:
   container: jakarta_servlet
   auths:
diff --git a/script/docker-compose/hertzbeat-mysql-iotdb/conf/application.yml 
b/script/docker-compose/hertzbeat-mysql-iotdb/conf/application.yml
index 7695271d03..f141461fb4 100644
--- a/script/docker-compose/hertzbeat-mysql-iotdb/conf/application.yml
+++ b/script/docker-compose/hertzbeat-mysql-iotdb/conf/application.yml
@@ -74,6 +74,16 @@ management:
       export:
         enabled: true
 
+# The generated OpenAPI document is a map of every route, HTTP method, 
parameter
+# and model, so it is not served by default. A deployment that wants the 
Swagger
+# UI opts in by turning both switches on; the document endpoints stay scoped to
+# the admin role in sureness.yml either way.
+springdoc:
+  api-docs:
+    enabled: false
+  swagger-ui:
+    enabled: false
+
 sureness:
   container: jakarta_servlet
   auths:
diff --git a/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml 
b/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
index 069fba3069..1815529be7 100644
--- a/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-mysql-iotdb/conf/sureness.yml
@@ -16,7 +16,7 @@
 ## -- sureness.yml account source -- ##
 
 # config the resource restful api that need auth protection, base rbac
-# rule: api===method===role 
+# rule: api===method===role
 # eg: /api/v1/source1===get===[admin] means /api/v2/host===post support 
role[admin] access.
 # eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by 
any role.
 resourceRole:
@@ -109,11 +109,18 @@ resourceRole:
   - /api/ai/**===delete===[admin]
   - /api/logs/sse/**===get===[admin,user,guest]
   - /api/logs/ingest/**===post===[admin,user]
+  # The OpenAPI document is a map of every route, parameter and model, so it is
+  # scoped like any other administrative resource instead of being anonymous
+  - /v3/api-docs/**===get===[admin]
+  - /v3/api-docs.yaml===get===[admin]
+  - /v3/api-docs.yaml/**===get===[admin]
+  - /v2/api-docs/**===get===[admin]
+  - /swagger-resources/**===get===[admin]
   # spring boot actuator exposes jvm, http and datasource internals for 
scraping
   - /actuator/**===get===[admin]
 
 # config the resource restful api that need bypass auth protection
-# rule: api===method 
+# rule: api===method
 # eg: /api/v1/source3===get means /api/v1/source3===get can be access by 
anyone, no need auth.
 excludedResource:
   - /api/alert/sse/**===*
@@ -147,10 +154,6 @@ excludedResource:
   - /**/*.json===get
   - /**/*.woff===get
   - /**/*.eot===get
-  # swagger ui resource
-  - /swagger-resources/**===get
-  - /v2/api-docs===get
-  - /v3/api-docs===get
   # h2 database
   - /h2-console/**===*
 
diff --git 
a/script/docker-compose/hertzbeat-mysql-tdengine/conf/application.yml 
b/script/docker-compose/hertzbeat-mysql-tdengine/conf/application.yml
index 90220d0669..f6aa53215a 100644
--- a/script/docker-compose/hertzbeat-mysql-tdengine/conf/application.yml
+++ b/script/docker-compose/hertzbeat-mysql-tdengine/conf/application.yml
@@ -74,6 +74,16 @@ management:
       export:
         enabled: true
 
+# The generated OpenAPI document is a map of every route, HTTP method, 
parameter
+# and model, so it is not served by default. A deployment that wants the 
Swagger
+# UI opts in by turning both switches on; the document endpoints stay scoped to
+# the admin role in sureness.yml either way.
+springdoc:
+  api-docs:
+    enabled: false
+  swagger-ui:
+    enabled: false
+
 sureness:
   container: jakarta_servlet
   auths:
diff --git a/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml 
b/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
index 069fba3069..1815529be7 100644
--- a/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-mysql-tdengine/conf/sureness.yml
@@ -16,7 +16,7 @@
 ## -- sureness.yml account source -- ##
 
 # config the resource restful api that need auth protection, base rbac
-# rule: api===method===role 
+# rule: api===method===role
 # eg: /api/v1/source1===get===[admin] means /api/v2/host===post support 
role[admin] access.
 # eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by 
any role.
 resourceRole:
@@ -109,11 +109,18 @@ resourceRole:
   - /api/ai/**===delete===[admin]
   - /api/logs/sse/**===get===[admin,user,guest]
   - /api/logs/ingest/**===post===[admin,user]
+  # The OpenAPI document is a map of every route, parameter and model, so it is
+  # scoped like any other administrative resource instead of being anonymous
+  - /v3/api-docs/**===get===[admin]
+  - /v3/api-docs.yaml===get===[admin]
+  - /v3/api-docs.yaml/**===get===[admin]
+  - /v2/api-docs/**===get===[admin]
+  - /swagger-resources/**===get===[admin]
   # spring boot actuator exposes jvm, http and datasource internals for 
scraping
   - /actuator/**===get===[admin]
 
 # config the resource restful api that need bypass auth protection
-# rule: api===method 
+# rule: api===method
 # eg: /api/v1/source3===get means /api/v1/source3===get can be access by 
anyone, no need auth.
 excludedResource:
   - /api/alert/sse/**===*
@@ -147,10 +154,6 @@ excludedResource:
   - /**/*.json===get
   - /**/*.woff===get
   - /**/*.eot===get
-  # swagger ui resource
-  - /swagger-resources/**===get
-  - /v2/api-docs===get
-  - /v3/api-docs===get
   # h2 database
   - /h2-console/**===*
 
diff --git 
a/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/application.yml 
b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/application.yml
index 0074a9dd3f..413bdb3718 100644
--- 
a/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/application.yml
+++ 
b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/application.yml
@@ -74,6 +74,16 @@ management:
       export:
         enabled: true
 
+# The generated OpenAPI document is a map of every route, HTTP method, 
parameter
+# and model, so it is not served by default. A deployment that wants the 
Swagger
+# UI opts in by turning both switches on; the document endpoints stay scoped to
+# the admin role in sureness.yml either way.
+springdoc:
+  api-docs:
+    enabled: false
+  swagger-ui:
+    enabled: false
+
 sureness:
   container: jakarta_servlet
   auths:
diff --git 
a/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml 
b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
index 069fba3069..1815529be7 100644
--- a/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-mysql-victoria-metrics/conf/sureness.yml
@@ -16,7 +16,7 @@
 ## -- sureness.yml account source -- ##
 
 # config the resource restful api that need auth protection, base rbac
-# rule: api===method===role 
+# rule: api===method===role
 # eg: /api/v1/source1===get===[admin] means /api/v2/host===post support 
role[admin] access.
 # eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by 
any role.
 resourceRole:
@@ -109,11 +109,18 @@ resourceRole:
   - /api/ai/**===delete===[admin]
   - /api/logs/sse/**===get===[admin,user,guest]
   - /api/logs/ingest/**===post===[admin,user]
+  # The OpenAPI document is a map of every route, parameter and model, so it is
+  # scoped like any other administrative resource instead of being anonymous
+  - /v3/api-docs/**===get===[admin]
+  - /v3/api-docs.yaml===get===[admin]
+  - /v3/api-docs.yaml/**===get===[admin]
+  - /v2/api-docs/**===get===[admin]
+  - /swagger-resources/**===get===[admin]
   # spring boot actuator exposes jvm, http and datasource internals for 
scraping
   - /actuator/**===get===[admin]
 
 # config the resource restful api that need bypass auth protection
-# rule: api===method 
+# rule: api===method
 # eg: /api/v1/source3===get means /api/v1/source3===get can be access by 
anyone, no need auth.
 excludedResource:
   - /api/alert/sse/**===*
@@ -147,10 +154,6 @@ excludedResource:
   - /**/*.json===get
   - /**/*.woff===get
   - /**/*.eot===get
-  # swagger ui resource
-  - /swagger-resources/**===get
-  - /v2/api-docs===get
-  - /v3/api-docs===get
   # h2 database
   - /h2-console/**===*
 
diff --git 
a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/application.yml 
b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/application.yml
index c25b533a48..2f59761b65 100644
--- a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/application.yml
+++ b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/application.yml
@@ -74,6 +74,16 @@ management:
       export:
         enabled: true
 
+# The generated OpenAPI document is a map of every route, HTTP method, 
parameter
+# and model, so it is not served by default. A deployment that wants the 
Swagger
+# UI opts in by turning both switches on; the document endpoints stay scoped to
+# the admin role in sureness.yml either way.
+springdoc:
+  api-docs:
+    enabled: false
+  swagger-ui:
+    enabled: false
+
 sureness:
   container: jakarta_servlet
   auths:
diff --git 
a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml 
b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
index d15a6ac5d2..6281d0927d 100644
--- a/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
+++ b/script/docker-compose/hertzbeat-postgresql-greptimedb/conf/sureness.yml
@@ -16,7 +16,7 @@
 ## -- sureness.yml account source -- ##
 
 # config the resource restful api that need auth protection, base rbac
-# rule: api===method===role 
+# rule: api===method===role
 # eg: /api/v1/source1===get===[admin] means /api/v2/host===post support 
role[admin] access.
 # eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by 
any role.
 resourceRole:
@@ -113,11 +113,18 @@ resourceRole:
   - /api/ingestion/otlp/**===get===[admin,user,guest]
   - /api/logs/**===get===[admin,user,guest]
   - /api/traces/**===get===[admin,user,guest]
+  # The OpenAPI document is a map of every route, parameter and model, so it is
+  # scoped like any other administrative resource instead of being anonymous
+  - /v3/api-docs/**===get===[admin]
+  - /v3/api-docs.yaml===get===[admin]
+  - /v3/api-docs.yaml/**===get===[admin]
+  - /v2/api-docs/**===get===[admin]
+  - /swagger-resources/**===get===[admin]
   # spring boot actuator exposes jvm, http and datasource internals for 
scraping
   - /actuator/**===get===[admin]
 
 # config the resource restful api that need bypass auth protection
-# rule: api===method 
+# rule: api===method
 # eg: /api/v1/source3===get means /api/v1/source3===get can be access by 
anyone, no need auth.
 excludedResource:
   - /api/alert/sse/**===*
@@ -154,10 +161,6 @@ excludedResource:
   - /**/*.json===get
   - /**/*.woff===get
   - /**/*.eot===get
-  # swagger ui resource
-  - /swagger-resources/**===get
-  - /v2/api-docs===get
-  - /v3/api-docs===get
   # h2 database
   - /h2-console/**===*
 
diff --git 
a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/application.yml
 
b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/application.yml
index 71d7fb2f58..9189445de0 100644
--- 
a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/application.yml
+++ 
b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/application.yml
@@ -74,6 +74,16 @@ management:
       export:
         enabled: true
 
+# The generated OpenAPI document is a map of every route, HTTP method, 
parameter
+# and model, so it is not served by default. A deployment that wants the 
Swagger
+# UI opts in by turning both switches on; the document endpoints stay scoped to
+# the admin role in sureness.yml either way.
+springdoc:
+  api-docs:
+    enabled: false
+  swagger-ui:
+    enabled: false
+
 sureness:
   container: jakarta_servlet
   auths:
diff --git 
a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml 
b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
index 069fba3069..1815529be7 100644
--- 
a/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
+++ 
b/script/docker-compose/hertzbeat-postgresql-victoria-metrics/conf/sureness.yml
@@ -16,7 +16,7 @@
 ## -- sureness.yml account source -- ##
 
 # config the resource restful api that need auth protection, base rbac
-# rule: api===method===role 
+# rule: api===method===role
 # eg: /api/v1/source1===get===[admin] means /api/v2/host===post support 
role[admin] access.
 # eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by 
any role.
 resourceRole:
@@ -109,11 +109,18 @@ resourceRole:
   - /api/ai/**===delete===[admin]
   - /api/logs/sse/**===get===[admin,user,guest]
   - /api/logs/ingest/**===post===[admin,user]
+  # The OpenAPI document is a map of every route, parameter and model, so it is
+  # scoped like any other administrative resource instead of being anonymous
+  - /v3/api-docs/**===get===[admin]
+  - /v3/api-docs.yaml===get===[admin]
+  - /v3/api-docs.yaml/**===get===[admin]
+  - /v2/api-docs/**===get===[admin]
+  - /swagger-resources/**===get===[admin]
   # spring boot actuator exposes jvm, http and datasource internals for 
scraping
   - /actuator/**===get===[admin]
 
 # config the resource restful api that need bypass auth protection
-# rule: api===method 
+# rule: api===method
 # eg: /api/v1/source3===get means /api/v1/source3===get can be access by 
anyone, no need auth.
 excludedResource:
   - /api/alert/sse/**===*
@@ -147,10 +154,6 @@ excludedResource:
   - /**/*.json===get
   - /**/*.woff===get
   - /**/*.eot===get
-  # swagger ui resource
-  - /swagger-resources/**===get
-  - /v2/api-docs===get
-  - /v3/api-docs===get
   # h2 database
   - /h2-console/**===*
 
diff --git a/script/sureness.yml b/script/sureness.yml
index d15a6ac5d2..6281d0927d 100644
--- a/script/sureness.yml
+++ b/script/sureness.yml
@@ -16,7 +16,7 @@
 ## -- sureness.yml account source -- ##
 
 # config the resource restful api that need auth protection, base rbac
-# rule: api===method===role 
+# rule: api===method===role
 # eg: /api/v1/source1===get===[admin] means /api/v2/host===post support 
role[admin] access.
 # eg: /api/v1/source2===get===[] means /api/v1/source2===get can not access by 
any role.
 resourceRole:
@@ -113,11 +113,18 @@ resourceRole:
   - /api/ingestion/otlp/**===get===[admin,user,guest]
   - /api/logs/**===get===[admin,user,guest]
   - /api/traces/**===get===[admin,user,guest]
+  # The OpenAPI document is a map of every route, parameter and model, so it is
+  # scoped like any other administrative resource instead of being anonymous
+  - /v3/api-docs/**===get===[admin]
+  - /v3/api-docs.yaml===get===[admin]
+  - /v3/api-docs.yaml/**===get===[admin]
+  - /v2/api-docs/**===get===[admin]
+  - /swagger-resources/**===get===[admin]
   # spring boot actuator exposes jvm, http and datasource internals for 
scraping
   - /actuator/**===get===[admin]
 
 # config the resource restful api that need bypass auth protection
-# rule: api===method 
+# rule: api===method
 # eg: /api/v1/source3===get means /api/v1/source3===get can be access by 
anyone, no need auth.
 excludedResource:
   - /api/alert/sse/**===*
@@ -154,10 +161,6 @@ excludedResource:
   - /**/*.json===get
   - /**/*.woff===get
   - /**/*.eot===get
-  # swagger ui resource
-  - /swagger-resources/**===get
-  - /v2/api-docs===get
-  - /v3/api-docs===get
   # h2 database
   - /h2-console/**===*
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to