diff --git a/backend/pom.xml b/backend/pom.xml index dd0bc03c..0dd83185 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -48,6 +48,11 @@ org.springframework.boot spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-micrometer-metrics + org.springframework.boot spring-boot-starter-validation diff --git a/backend/src/main/java/org/raddatz/familienarchiv/FamilienarchivApplication.java b/backend/src/main/java/org/raddatz/familienarchiv/FamilienarchivApplication.java index 09227d27..4fef338f 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/FamilienarchivApplication.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/FamilienarchivApplication.java @@ -1,11 +1,9 @@ package org.raddatz.familienarchiv; import org.springframework.boot.SpringApplication; -import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; -// Excluded: management port (8081) is network-isolated inside archiv-net; no app-level auth needed. -@SpringBootApplication(exclude = {ManagementWebSecurityAutoConfiguration.class}) +@SpringBootApplication public class FamilienarchivApplication { public static void main(String[] args) { diff --git a/backend/src/main/java/org/raddatz/familienarchiv/security/SecurityConfig.java b/backend/src/main/java/org/raddatz/familienarchiv/security/SecurityConfig.java index 298d9fa6..2cf85573 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/security/SecurityConfig.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/security/SecurityConfig.java @@ -54,8 +54,14 @@ public class SecurityConfig { .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> { - // Health endpoint must be open so CI/Docker health checks work without credentials - auth.requestMatchers("/actuator/health").permitAll(); + // Both /actuator/health and /actuator/prometheus must be open. + // In Spring Boot 4.0 the management server (port 8081) shares the security filter chain; + // network isolation (port 8081 not published in docker-compose) is the security boundary. + // Health and Prometheus must be open — no credentials for Docker health checks or Prometheus scraping. + // Note: in Spring Boot 4.0 the management port shares the security filter chain, + // so these paths must be explicitly permitted here even though they are served on port 8081. + // Network isolation (port 8081 not published in docker-compose) is the outer security boundary. + auth.requestMatchers("/actuator/health", "/actuator/prometheus").permitAll(); // Password reset endpoints are unauthenticated by nature auth.requestMatchers("/api/auth/forgot-password", "/api/auth/reset-password").permitAll(); // Invite-based registration endpoints are public diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index ead3d9e8..5f4fb207 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -49,7 +49,8 @@ management: # Management port is separate from the app port so that: # (a) Caddy never proxies /actuator/* (it only routes :8080 → the app port) # (b) Prometheus scrapes backend:8081 directly inside archiv-net, not via Caddy - # (c) Spring Security's session-authenticated filter chain on :8080 never sees actuator requests + # Note: in Spring Boot 4.0 the management port shares the security filter chain; /actuator/health + # and /actuator/prometheus must be explicitly permitted in SecurityConfig — see SecurityConfig.java. port: 8081 endpoints: web: @@ -58,6 +59,11 @@ management: endpoint: prometheus: enabled: true + # Spring Boot 4.0: metrics export is disabled by default — explicitly opt in for Prometheus + prometheus: + metrics: + export: + enabled: true health: mail: enabled: false diff --git a/backend/src/test/java/org/raddatz/familienarchiv/ActuatorPrometheusIT.java b/backend/src/test/java/org/raddatz/familienarchiv/ActuatorPrometheusIT.java new file mode 100644 index 00000000..36d91bfa --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/ActuatorPrometheusIT.java @@ -0,0 +1,48 @@ +package org.raddatz.familienarchiv; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalManagementPort; +import org.springframework.context.annotation.Import; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.web.client.DefaultResponseErrorHandler; +import org.springframework.web.client.RestTemplate; +import software.amazon.awssdk.services.s3.S3Client; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +@Import(PostgresContainerConfig.class) +class ActuatorPrometheusIT { + + @LocalManagementPort + private int managementPort; + + @MockitoBean + S3Client s3Client; + + @Test + void prometheus_endpoint_returns_jvm_metrics_without_credentials() { + ResponseEntity response = noThrowTemplate().getForEntity( + "http://localhost:" + managementPort + "/actuator/prometheus", String.class); + + assertThat(response.getStatusCode().value()).isEqualTo(200); + assertThat(response.getBody()).contains("jvm_memory_used_bytes"); + } + + private RestTemplate noThrowTemplate() { + RestTemplate template = new RestTemplate(); + template.setErrorHandler(new DefaultResponseErrorHandler() { + @Override + public boolean hasError(org.springframework.http.client.ClientHttpResponse response) throws IOException { + return false; + } + }); + return template; + } +}