Fix Spring Boot Health Endpoints 401 in Azure App Service with Forwarded Headers

Summary

A Spring Boot application utilizing Auth0 OAuth2 Resource Server protection failed to allow unauthenticated access to /health and /actuator/health endpoints after deployment to Azure App Service. Despite explicit .permitAll() configuration in the SecurityFilterChain, the application returned 401 Unauthorized in the cloud environment, while functioning perfectly in local development.

Root Cause

The root cause is Path Prefixing/Rewriting or Proxy Header Manipulation performed by the Azure App Service infrastructure (or an associated Application Gateway/Front Door).

  • Path Mismatch: Azure App Service often acts as a reverse proxy. If the platform or a configured rule rewrites the incoming request path (e.g., stripping a virtual directory prefix or adding one), the path seen by the Spring Boot application no longer matches the literal strings /health or /actuator/health defined in the requestMatchers.
  • Filter Chain Bypass Failure: When the path is modified by a proxy before it reaches the Servlet container, Spring Security’s AntPathRequestMatcher fails to find a match for the “permitted” paths and falls through to the .anyRequest().authenticated() rule.
  • Security Filter Order vs. Proxy Logic: The 401 is triggered because the application treats the incoming request as a standard protected resource because the effective URI in the Spring Context does not align with the configured URI in the security policy.

Why This Happens in Real Systems

In local development, the application sits directly on the network interface. The URI you type in your browser is exactly what the Spring DispatcherServlet receives. In production:

  • Reverse Proxies: Load balancers and ingress controllers (like those used in Azure) often modify headers like X-Forwarded-Prefix or rewrite paths to route traffic to specific microservices.
  • Virtual Directories: Cloud platforms often host multiple apps under a single domain, requiring path-based routing that alters the request URI.
  • Implicit Security Layers: Azure App Service might have “Easy Auth” (App Service Authentication) enabled at the platform level, which intercepts requests before they even reach the Spring Boot application.

Real-World Impact

  • Broken Observability: Automated health checkers (Azure Monitor, Kubernetes Liveness probes) fail, causing the platform to mark the instance as unhealthy and trigger unnecessary restarts or “death spirals.”
  • False Positives in Monitoring: High error rates in dashboards due to 401s on monitoring endpoints, masking actual application errors.
  • Deployment Failures: Zero-downtime deployments (Blue/Green) may fail if the deployment controller cannot verify the health of the new slot.

Example or Code

To fix this, we must ensure Spring Boot is “Proxy Aware” so it uses the original URI for security matching, and we must use more robust path matching.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            // Ensure the application respects X-Forwarded-* headers
            .apply(new ForwardedHeaderFilter()) 
            .authorizeHttpRequests(auth -> auth
                // Use more flexible pattern matching
                .requestMatchers("/health/**", "/actuator/**").permitAll()
                .requestMatchers("/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
            )
            .csrf(AbstractHttpConfigurer::disable);

        return http.build();
    }
}

In application.yml:

server:
  forward-headers-strategy: framework

How Senior Engineers Fix It

  1. Enable Forwarded Header Support: Configure server.forward-headers-strategy=framework. This tells Spring to look at X-Forwarded-Prefix and X-Forwarded-Proto headers to reconstruct the “true” request URI.
  2. Implement Pattern Matching Robustness: Instead of exact string matches like /health, use wildcard patterns like /health/** to account for potential sub-paths added by proxies.
  3. Verify Infrastructure Headers: Use a tool like curl -v or inspect Azure logs to see if the platform is injecting a prefix (e.g., /myapp/actuator/health).
  4. Audit Platform-Level Auth: Check if Azure App Service Authentication (Easy Auth) is enabled. If it is, the 401 is being thrown by Azure’s infrastructure, not Spring Boot, and must be bypassed via “Allow anonymous requests” in the Azure Portal settings.

Why Juniors Miss It

  • The “Works on My Machine” Fallacy: Juniors often assume that if the code is identical, the behavior must be identical, ignoring the environmental context (the network topology).
  • Focusing on Application Logic Only: They spend hours debugging the SecurityFilterChain logic or the AudienceValidator because the error is a 401, assuming the issue is with the JWT or the code, rather than the request routing.
  • Ignoring Middleware: They treat the application as an isolated island, forgetting that in production, the application is the last step in a long chain of proxies, load balancers, and gateways.

Leave a Comment