[ISSUE-149] Fix actuator security to allow unauthenticated health/info on management port #161
No reviewers
Labels
No Label
architect:complete
blocked-by:#139
blocks:#140
bug
CI/CD
complexity:low
complexity:medium
complexity:medium
config
depends-on:#124
docker
docs
effort:l
effort:s
effort:xs
epic/development
lead:complete
needs-decision
performance
priority:critical
priority:high
priority:low
priority:medium
security
spike
story-points:1
story-points:3
story-points:5
story-points:8
tech-debt
test
user-story
architect:complete
complexity:high
complexity:low
complexity:medium
cross-repo
cross-repo-dev
dev:in-progress
effort:l
effort:m
effort:s
effort:xl
effort:xs
epic
analytics
epic
development
epic
devops
epic
infra
epic
observability
epic
platform
epic
product
lead:complete
needs-decision
pipeline-complete
pipeline-error
pipeline-running
priority
later
priority
next
priority
now
start-pipeline
status
acceptance
status
blocked
status
done
status
in progress
status
in review
status
in testing
status
ready
status
refine
status
triage
subtask
type
analysis
type
bug
type
hygiene
type
mantainence
type
story
user-story
No Milestone
No project
No Assignees
3 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Hithomelabs/CFTunnels#161
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "Dave/CFTunnels:ISSUE-149"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Fix Spring Boot Actuator health endpoint returning OIDC authentication redirect instead of health status JSON.
The health endpoint at
http://192.168.0.100:5004/actuator/healthwas returning an authentication redirect because the catch-allSecurityFilterChaininSecuirtyConfig.javawas intercepting actuator requests on the separate management port (8081) before theActuatorSecurityConfigfilter chain could apply.Root Cause
SecuirtyConfighas nosecurityMatcher, making it a catch-all that matches ALL requestsmanagement.server.port=8081, Spring Boot creates a separate management serverActuatorSecurityConfighad@Order(1)on the@Beanmethod, which may not be sufficient to override auto-configured management security filter chainssecurityMatcher("/actuator/**")may not work correctly across separate servlet contextsChanges
@Order(Ordered.HIGHEST_PRECEDENCE)at class level to guarantee it's evaluated firstsecurityMatcherwithEndpointRequest.toAnyEndpoint()for proper Spring Boot actuator endpoint matchingpermitAll()for/actuator/healthand/actuator/infobefore the catch-allanyRequest().authenticated()Issues
Testing
http://192.168.0.100:5004/actuator/healthreturns JSON health status without authenticationhttp://192.168.0.100:5004/actuator/inforeturns info without authenticationPR Review: Fix Actuator Security for Unauthenticated Health/Info on Management Port
Strengths
Correct root cause analysis: The PR description clearly identifies the issue —
SecuirtyConfighas nosecurityMatcher, making it a catch-all that intercepts ALL requests including actuator endpoints on the separate management port (8081). Well-documented.EndpointRequest.toAnyEndpoint()is the right choice: Replacing the string-based"/actuator/**"withEndpointRequest.toAnyEndpoint()is the idiomatic Spring Boot approach. It properly handles the management server servlet context and the actuator base path configuration.Excellent Javadoc: The updated class-level documentation is thorough — it explains the ordering rationale, references the specific auto-configuration being overridden, and describes the failure scenario. This is the kind of documentation that saves future developers hours of debugging.
Belt-and-suspenders defense: Adding
.requestMatchers("/actuator/health", "/actuator/info").permitAll()inSecuirtyConfigis a smart defensive measure. Withmanagement.server.port=8081, actuator requests won't normally reachSecuirtyConfig, but this ensures resilience if the management port configuration changes in the future.Least-privilege principle preserved: Only
/actuator/healthand/actuator/infoare permitted without auth. All other actuator endpoints remain authenticated via.anyRequest().authenticated()in both filter chains.show-details=when-authorizedinapplication.propertiesis correctly configured — unauthenticated requests get minimal health status, authenticated requests get full component details.Concerns
Ordered.HIGHEST_PRECEDENCEmay be overly aggressive (minor): This evaluates toInteger.MIN_VALUE. For this codebase with only 2 security configs it's fine, but if other security filter chains are added later (e.g., for API key auth, basic auth), they could conflict. Consider using@Order(0)instead — it's the conventional "go first" order and leaves room for truly highest-precedence chains.CSRF disabled on actuator chain: This is standard for REST actuator endpoints, but worth noting in a comment for future auditors.
No integration tests: Security configuration is one of the highest-risk areas. Consider adding a test that verifies:
GET /actuator/healthon management port returns 200 without authGET /actuator/infoon management port returns 200 without authGET /actuator/beanson management port returns 401/403 without authGET /actuator/healthon main port returns 200 without auth (belt-and-suspenders)Testing checklist not verified: The PR body has all testing items unchecked. These should be verified before merge — especially the port-specific tests.
Recommendations
Consider
@Order(0)overHIGHEST_PRECEDENCE: More conventional, leaves headroom, and achieves the same effect for this codebase. Not blocking.Add a brief comment above the
SecuirtyConfigpermitAll explaining it's a fallback for non-separated-port configurations:Verify the fix in production: After merge, confirm with Uptime Kuma that health checks pass without authentication on port 5004 (mapped to 8081).
Future improvement: Consider adding a
@SuppressWarningsor renaming the typo inSecuirtyConfig→SecurityConfig(pre-existing, not blocking this PR).Effort Estimate
Approval Status
APPROVED — This is a clean, well-documented fix that correctly addresses the root cause. The changes are minimal, focused, and use the idiomatic Spring Boot approach. Minor recommendations above are non-blocking. Recommend merging after verifying the testing checklist items.
@ -15,0 +18,4 @@* Uses @Order(Ordered.HIGHEST_PRECEDENCE) to ensure this filter chain is evaluated BEFORE* any auto-configured management security filter chains (e.g., ManagementWebSecurityAutoConfiguration).* Without this, the catch-all SecurityFilterChain in SecuirtyConfig can intercept actuator* requests on the separate management port (management.server.port=8081) and redirect to OIDC login.Using
Ordered.HIGHEST_PRECEDENCE(Integer.MIN_VALUE) is correct but aggressive. Consider@Order(0)instead — it's the conventional 'go first' order and leaves headroom if other security filter chains are added later. Not blocking for this PR, but a consideration for future-proofing.@ -30,3 +30,4 @@.authorizeHttpRequests(auth -> auth//.requestMatchers( "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html" ).permitAll().requestMatchers("/actuator/health", "/actuator/info").permitAll().anyRequest().authenticated()Good belt-and-suspenders approach. With
management.server.port=8081, actuator requests won't normally reach this filter chain, but this serves as a safety net. Consider adding a brief comment explaining the fallback intent:🏛️ Architect Review — PR #161: Actuator Security Fix
Posted by @Archie (architect). Note: architect token is scoped to
write:issue(nowrite:repository), so this is posted as a thread comment rather than a formal review; @Linus's formal APPROVED review stands.Verdict: APPROVED ✅
Root Cause — Confirmed & Sharpened
The PR's diagnosis is correct, and I can pin the definitive mechanism: Spring Security 6.1+ no longer honors
@Orderon@Beanmethods (Boot 3.4.5 ships Spring Security 6.4.x). The original@Order(1)onactuatorSecurityChain(...)was silently ignored, leaving both chains at default order (LOWEST_PRECEDENCE). With ties,FilterChainProxyfalls back to bean registration order, and the catch-allSecuirtyConfigchain won for actuator requests on the management port → OIDC redirect. Moving@Orderto the class level is the mandatory, idiomatic fix — not stylistic.One Javadoc nuance to correct (non-blocking):
ManagementWebSecurityAutoConfigurationbacks off entirely when anySecurityFilterChainbean exists (@ConditionalOnMissingBean). So this is intra-app chain ordering inside the singleFilterChainProxy, not "overriding auto-configured management chains". The separate management port (8081) shares the parent context's chains (child context'sspringSecurityFilterChaindelegating filter resolves to the parent proxy) — which is exactly why ordering within that proxy is what fixes the health endpoint. Fix is correct; the Javadoc just slightly mischaracterizes the mechanism.What's Right
EndpointRequest.toAnyEndpoint()is the idiomatic matcher — context-aware (main + management servlet contexts) and resilient tomanagement.endpoints.web.base-pathchanges.health/infopermitted;.anyRequest().authenticated()future-proofs newly exposed actuator endpoints (better than the issue template's.anyRequest().permitAll()).show-details=when-authorized— unauthenticated probes get{"status":"UP"}with no component/DB detail leakage. Correct security posture.health,info— minimal attack surface on the management port.permitAllinSecuirtyConfigis inert-but-safe: withmanagement.server.port=8081the main port serves no actuator endpoints, so behavior changes from "302 OIDC redirect" → "404" — friendlier for probes.Concerns & Recommendations (non-blocking)
Ordered.HIGHEST_PRECEDENCEis aggressive (Integer.MIN_VALUE). Fine with 2 chains, but@Order(0)— or even@Order(1)at class level — achieves identical behavior here while leaving headroom for future chains (API-key/basic-auth) that may legitimately need to run first. Both defensible.docker-compose.yamlmaps${MNGT_PORT:-5003}:8081(default 5003), but the PR body says 5004. Verifystack.env/.envsetsMNGT_PORT=5004and Uptime Kuma points at the actual published port — otherwise 5003 is the effective default.application-integration.properties). Add tests: unauthenticated 200 on/actuator/health+/actuator/info(management port), 401/403 on a non-permitted actuator endpoint, and preserved OIDC redirect on main app endpoints. Security config is where silent regressions happen.SecuirtyConfigtypo → rename toSecurityConfigin a separate refactor PR (keeps this diff minimal).Effort Estimate
Recommendation
Merge after verifying the unchecked testing items — particularly the actual published management port (5003 vs 5004) and the Uptime Kuma target URL.