Spring Security with HTTPS on CloudFoundry

2020-02-14 08:18发布

问题:

I tried to access my application on CloudFoundry with the following configuration in the spring security xml

<intercept-url pattern="/signup*" access="permitAll" requires-channel="https" />

but it gives me error This webpage has a redirect loop

However when I changed it to requires-channel="http" I can see my page normally. In both cases I used https on my application. Is this the expected behavior ?

回答1:

I have the same issue when I tried to secure my pages with HTTPS using Spring Security.

From the discussion on CloudFoundry Support, seems they "terminate SSL connections at the router". See "Is it possible to visit my application via SSL (HTTPS)?".

And after more than a year, no further information I can find regarding this issue.



回答2:

First of all, taking a step back, this (https://johnpfield.wordpress.com/2014/09/10/configuring-ssltls-for-cloud-foundry/) provides excellent context for the nature of the problem.

The key paragraph being

“The threat model here is that the entry point to the cloud is a high availability, secured proxy server.  Once the traffic traverses that perimeter, it is on a trusted subnet.  In fact, the actual  IP address and port number where the Tomcat application server are running are not visible from outside of the cloud. The only way to get an HTTP request to that port is to go via the secure proxy. This pattern is a well established best practice amongst security architecture practitioners.”

Therefore, we may not want or need SSL all the way down, but read on to see how to avoid the https redirect issue when using Spring Security deployed on Cloud Foundry.

You will have a load balancer, HAProxy or some kind of proxy terminating SSL at the boundary of your Cloud Foundry installation. As a convention, whatever you are using should be configured to set X-Forwarded-For and X-Forwarded-Proto headers. The request header “X-Forwarded-Proto" contains the value http or https depending on the original request and you need to use this header parameter for your security decisions further down the stack.

The cleanest way to do this is at the container level, so that Spring Security behaves the same independent of deployment container. Some typical options to configure this are as follows

1) Tomcat

Tomcat should be configured with a RemoteIPValve as described nicely here

The good news is that the Java buildpack for Cloud Foundry already does this for you as seen here

2) Spring Boot (Embedded Tomcat)

Because Tomcat is embedded, the Tomcat config in the Java buildpack will not be activated (see the buildpack Detection Criterion), and therefore some internal Spring Boot configuration is required. Luckily, it’s pretty trivial to configure as you would expect with Spring Boot and you can switch on Tomcat’s RemoteIPValve as explained here by simply defining

server.tomcat.remote_ip_header=x-forwarded-for

server.tomcat.protocol_header=x-forwarded-proto

Both approaches lead to the same outcome of the Tomcat valve overriding the ServletRequest.isSecure() behaviour so that the application has no knowledge of the usage of any proxying. Note that the valve will only be used when the “X-Forwarded-Proto" header is set.

Alternatively, if you really want to go low-level you can dig into the guts of Spring Security, as demonstrated here. As part of that effort, there are some useful findings on how to make the “X-Forwarded-Proto" header available via the Servlet API for other containers (WebLogic, JBoss, Jetty, Glassfish) shared on the comments of https://github.com/BroadleafCommerce/BroadleafCommerce/issues/424

As an additional note, CloudFlare can also act as the SSL-terminating reverse proxy (this is the recommended approach via PWS as discussed here) and it does indeed forward the relevant headers.

References

https://stackoverflow.com/a/28300485/752167

http://experts.hybris.com/answers/33612/view.html

https://github.com/cloudfoundry/java-buildpack/commit/540633bc932299ef4335fde16d4069349c66062e

https://support.run.pivotal.io/entries/24898367-Spring-security-with-https

http://docs.spring.io/spring-boot/docs/current/reference/html/howto-embedded-servlet-containers.html#howto-use-tomcat-behind-a-proxy-server



回答3:

In case it's still useful ... I found this post gave the clue to solve something similar to this.

The problem was the org.springframework.security.web.access.channel.SecureChannelProcessor bean was using ServletRequest.isSecure() to decide whether to accept the connection or redirect, which was getting confused inside the cloud.

The following override to that bean seemed to do the job under BlueMix - not sure if the $WSSC request header will apply to all environments.

@Component
public class ChannelProcessorsPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException {
        if (bean instanceof SecureChannelProcessor) {
            final SecureChannelProcessor scp = (SecureChannelProcessor) bean;
            return new ChannelProcessor() {
                @Override
                public void decide(FilterInvocation invocation,
                        Collection<ConfigAttribute> config) throws IOException,
                        ServletException {
                    HttpServletRequest httpRequest = invocation.getHttpRequest();
                    // Running under BlueMix (CloudFoundry in general?), the
                    //   invocation.getHttpRequest().isSecure() in SecureChannelProcessor
                    //   was always returning false
                    if ("https".equals(httpRequest.getHeader("$WSSC"))) {
                        return;
                    }
                    scp.decide(invocation, config);
                }

                @Override
                public boolean supports(ConfigAttribute attribute) {
                    return scp.supports(attribute);
                }
            };
        }

        return bean;
    }

    @Override
    public Object postProcessBeforeInitialization(final Object bean, final String beanName) throws BeansException {

        return bean;
    }
}