diff --git a/pom.xml b/pom.xml
index a01a669..f938231 100644
--- a/pom.xml
+++ b/pom.xml
@@ -131,7 +131,7 @@
spring-boot-starter-security
-
+
org.springframework.boot
spring-boot-starter-actuator
diff --git a/src/main/kotlin/com/embabel/guide/chat/security/SecurityConfig.kt b/src/main/kotlin/com/embabel/guide/chat/security/SecurityConfig.kt
index 1d316e1..6654939 100644
--- a/src/main/kotlin/com/embabel/guide/chat/security/SecurityConfig.kt
+++ b/src/main/kotlin/com/embabel/guide/chat/security/SecurityConfig.kt
@@ -47,6 +47,11 @@ class SecurityConfig(
web.ignoring().requestMatchers(*mcpMatchers)
}
+ val actuatorPatterns = arrayOf(
+ "/actuator",
+ "/actuator/**",
+ )
+
val permittedPatterns = arrayOf(
"/ws/**",
"/app/**",
@@ -55,8 +60,7 @@ class SecurityConfig(
"/",
"/index.html",
"/static/**",
- "/actuator/**",
- ) + mcpPatterns
+ ) + mcpPatterns + actuatorPatterns
@Bean
@Order(0)
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 9d3e844..c9ed59d 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,6 +1,17 @@
server:
port: 1337
+# Actuator endpoints for health checks and monitoring
+management:
+ endpoints:
+ web:
+ exposure:
+ include: health,info
+ base-path: /actuator
+ endpoint:
+ health:
+ show-details: when-authorized
+
guide:
# If true, the Guide chatbot will load content from URLs and directories on startup
diff --git a/src/test/kotlin/com/embabel/guide/chat/security/ActuatorSecurityTest.kt b/src/test/kotlin/com/embabel/guide/chat/security/ActuatorSecurityTest.kt
new file mode 100644
index 0000000..29ddc36
--- /dev/null
+++ b/src/test/kotlin/com/embabel/guide/chat/security/ActuatorSecurityTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024-2025 Embabel Software, Inc.
+ *
+ * Licensed 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 com.embabel.guide.chat.security
+
+import com.embabel.guide.Neo4jPropertiesInitializer
+import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.ActiveProfiles
+import org.springframework.test.context.ContextConfiguration
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
+
+/**
+ * Tests that Actuator health endpoints are accessible without authentication.
+ *
+ * These tests ensure that health check endpoints remain publicly accessible
+ * for monitoring, orchestration, and CI/CD workflows.
+ */
+@SpringBootTest
+@ActiveProfiles("test")
+@AutoConfigureMockMvc
+@ContextConfiguration(initializers = [Neo4jPropertiesInitializer::class])
+class ActuatorSecurityTest {
+
+ @Autowired
+ private lateinit var mockMvc: MockMvc
+
+ @Test
+ fun `actuator health endpoint should be accessible without authentication`() {
+ val result = mockMvc.perform(get("/actuator/health")).andReturn()
+ val httpStatus = result.response.status
+ // Health endpoint should not be blocked by security (401/403)
+ // It may return 503 if some health indicators are DOWN, but security should not block it
+ assert(httpStatus != 401 && httpStatus != 403) {
+ "Actuator health endpoint should not return 401 or 403, got $httpStatus"
+ }
+ }
+
+ @Test
+ fun `actuator health endpoint should return health status`() {
+ val result = mockMvc.perform(get("/actuator/health")).andReturn()
+ val httpStatus = result.response.status
+ // Should be 200 (UP) or 503 (DOWN), but accessible
+ assert(httpStatus == 200 || httpStatus == 503) {
+ "Actuator health endpoint should return 200 or 503, got $httpStatus"
+ }
+ // Response should contain status field
+ val body = result.response.contentAsString
+ assert(body.contains("status")) {
+ "Health response should contain status field"
+ }
+ }
+
+ @Test
+ fun `actuator info endpoint should be accessible without authentication`() {
+ val result = mockMvc.perform(get("/actuator/info")).andReturn()
+ val httpStatus = result.response.status
+ // Info endpoint might be empty (404) or return data (200), but should not be blocked by security (401/403)
+ assert(httpStatus != 401 && httpStatus != 403) {
+ "Actuator info endpoint should not return 401 or 403, got $httpStatus"
+ }
+ }
+
+ @Test
+ fun `actuator base path should be accessible`() {
+ val result = mockMvc.perform(get("/actuator")).andReturn()
+ val httpStatus = result.response.status
+ // Should not be blocked by security
+ assert(httpStatus != 401 && httpStatus != 403) {
+ "Actuator base path should not return 401 or 403, got $httpStatus"
+ }
+ }
+}