We're using Spring Boot 2.7.3 with the version of Actuator (spring-boot-starter-actuator) that is shipped with 2.7.3. We're exposing Actuator on port 8081 under the /management context path. In production, our application is running behind a proxy that sets several X-Forwarded-* headers, including the X-Forwarded-Prefix header that is set to /service. But when navigating to https://www.company.com/management this is what is returned:

{
    "_links": {
        "self": {
            "href": "https://www.company.com/management",
            "templated": false
        },
        "beans": {
            "href": "https://www.company.com/management/beans",
            "templated": false
        },
        "caches-cache": {
            "href": "https://www.company.com/management/caches/{cache}",
            "templated": true
        },
        "caches": {
            "href": "https://www.company.com/management/caches",
            "templated": false
        },
        "health": {
            "href": "https://www.company.com/management/health",
            "templated": false
        },
        "health-path": {
            "href": "https://www.company.com/management/health/{*path}",
            "templated": true
        },
        "info": {
            "href": "https://www.company.com/management/info",
            "templated": false
        },
        "conditions": {
            "href": "https://www.company.com/management/conditions",
            "templated": false
        },
        "configprops": {
            "href": "https://www.company.com/management/configprops",
            "templated": false
        },
        "configprops-prefix": {
            "href": "https://www.company.com/management/configprops/{prefix}",
            "templated": true
        },
        "env": {
            "href": "https://www.company.com/management/env",
            "templated": false
        },
        "env-toMatch": {
            "href": "https://www.company.com/management/env/{toMatch}",
            "templated": true
        },
        "integrationgraph": {
            "href": "https://www.company.com/management/integrationgraph",
            "templated": false
        },
        "loggers": {
            "href": "https://www.company.com/management/loggers",
            "templated": false
        },
        "loggers-name": {
            "href": "https://www.company.com/management/loggers/{name}",
            "templated": true
        },
        "heapdump": {
            "href": "https://www.company.com/management/heapdump",
            "templated": false
        },
        "threaddump": {
            "href": "https://www.company.com/management/threaddump",
            "templated": false
        },
        "metrics-requiredMetricName": {
            "href": "https://www.company.com/management/metrics/{requiredMetricName}",
            "templated": true
        },
        "metrics": {
            "href": "https://www.company.com/management/metrics",
            "templated": false
        },
        "scheduledtasks": {
            "href": "https://www.company.com/management/scheduledtasks",
            "templated": false
        },
        "sessions-sessionId": {
            "href": "https://www.company.com/management/sessions/{sessionId}",
            "templated": true
        },
        "sessions": {
            "href": "https://www.company.com/management/sessions",
            "templated": false
        },
        "mappings": {
            "href": "https://www.company.com/management/mappings",
            "templated": false
        },
        "refresh": {
            "href": "https://www.company.com/management/refresh",
            "templated": false
        },
        "features": {
            "href": "https://www.company.com/management/features",
            "templated": false
        },
        "traces": {
            "href": "https://www.company.com/management/traces",
            "templated": false
        }
    }
}

I'm expecting the href's in the response to start with https://www.company.com/service due to the supplied X-Forwarded-Prefix header, but this is not the case. I've tried to add the ForwardedHeaderFilter like this:

@Bean
public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilterFilterRegistrationBean() {
    ForwardedHeaderFilter forwardedHeaderFilter = new ForwardedHeaderFilter();
    FilterRegistrationBean<ForwardedHeaderFilter> bean = new FilterRegistrationBean<>(forwardedHeaderFilter);
    bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return bean;
}

but it makes no difference. I've posted this as a question on stackoverflow and it seem to work after applying this workaround:

@Component
@ConditionalOnManagementPort(ManagementPortType.DIFFERENT)
public class ManagementContextFactoryBeanPostProcessor
        implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        if (bean instanceof ManagementContextFactory managementContextFactory) {
            return (ManagementContextFactory) (parent, configurationClasses) -> {
                var context = managementContextFactory.createManagementContext(parent, configurationClasses);
                if (context instanceof GenericWebApplicationContext genericWebApplicationContext) {
                    genericWebApplicationContext.registerBean(ForwardedHeaderFilterRegistrationBean.class);
                }
                return context;
            };
        }
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    public static class ForwardedHeaderFilterRegistrationBean
            extends FilterRegistrationBean<ForwardedHeaderFilter> {

        public ForwardedHeaderFilterRegistrationBean() {
            setFilter(new ForwardedHeaderFilter());
            setOrder(Ordered.HIGHEST_PRECEDENCE);
        }

    }
}

Is it expected behavior that the ForwardedHeaderFilter is only applied to non-actuator requests or could it be a bug?

For context, in another application that uses Spring Webflux, we're using the org.springframework.web.server.adapter.ForwardedHeaderTransformer like this:

@Bean
fun forwardedHeaderTransformer() = ForwardedHeaderTransformer()

and this seems to affect actuator requests as well. So, to me, it's a bit confusing since it doesn't seem consistent with the way ForwardedHeaderFilter seems to work :)

Comment From: bclozel

I think this is closely related to https://github.com/spring-projects/spring-boot/issues/32173#issuecomment-1227162718 and #31811. This issue is not a total duplicate, but triggers the following question: should we apply Servlet Filter and WebFilter in general for the management context, or not.

We could argue that it would be consistent for things to behave the same way, whether there is a separate port or not. But on a case by case basis, this might not work:

  • metrics filters should not instrument calls to the metrics endpoint
  • depending on the case, forwarded header filters should not be applied on a management port - :8080 receives public traffic and should deal with those headers, whereas :8081 is typically exposed internally and is not concerned with forwarded traffic.

Comment From: bclozel

I'm closing as a duplicate of #31811. Both discussions have interesting points but let's try to focus on a single issue for this. Thanks for the report!