Nginx/SpringBoot/Kubernetes - X-Forwarded-For Header For Client IP

3/10/2021

A bit inexperienced at this, so looking for some help on how I can do this! Sorry if it is unclear of what I'm looking to do.

Objective

I have an Angular front-end that is location based. I am hoping to be able to use the users public IP by taking it and using a geolocation service to give me the city/region that they are from.

Update #1

From one of the answers below, I now am getting an IP address in SpringBoot, but unfortunately it is the IP address of the DigitalOcean droplet.

Current Setup

I am using a Spring Security Custom Filter to perform this action. This sits behind the Angular application.

I was hoping that I would be able to use the HttpServletRequest request.getRemoteAddr() to get the IP address, but I have found that once the SpringBoot application is deployed on Kubernetes, which sits behind an NGINX proxy, the getRemoteAddr() gives me the Digital Ocean droplet IP.

Due to this, I was hoping I would be able to pass this client IP address forward as the X-Forwarded-For header, or even a custom X-Client-IP header. How would I go about this if I'm performing these actions as part of a Spring Security Filter? Is it even possible?

Nginx Config

location / {
    proxy_set_header    Host               $host;
    proxy_set_header    X-Client-IP        $proxy_add_x_forwarded_for;
    proxy_set_header    X-Real-IP          $remote_addr;
    proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;
    proxy_set_header    X-Forwarded-Host   $host;
    proxy_set_header    X-Forwarded-Server $host;
    proxy_set_header    X-Forwarded-Port   $server_port;
    proxy_set_header    X-Forwarded-Proto  $scheme;

    rewrite .* /index.html break;
  }

Spring Boot Filter

private static String getClientIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }

        return ip;
    }

All of these return null apart from request.getRemoteAddr() (which returns the loadbalancer IP).

Kubernetes Setup

kind: Deployment
apiVersion: apps/v1
metadata:
  name: example-webapp-frontend-deployment
spec:
  revisionHistoryLimit: 3
  minReadySeconds: 30
  replicas: 1
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: example-webapp-frontend
  template:
    metadata:
      labels:
        app: example-webapp-frontend
    spec:
      restartPolicy: Always
      containers:
        - name: example-webapp-frontend
          image: docker-example/example-webapp-frontend:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 80
      imagePullSecrets:
        - name: docker-creds

---

apiVersion: v1
kind: Service
metadata:
  name: example-webapp-frontend-service
spec:
  selector:
    app: example-webapp-frontend
  ports:
    - protocol: TCP
      targetPort: 80
      port: 80

---

kind: Deployment
apiVersion: apps/v1
metadata:
  name: example-webapp-bff-deployment
spec:
  revisionHistoryLimit: 3
  minReadySeconds: 30
  replicas: 1
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: example-webapp-bff
  template:
    metadata:
      labels:
        app: example-webapp-bff
    spec:
      restartPolicy: Always
      containers:
        - name: example-webapp-bff
          image: docker-example/example-webapp-bff:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 9874
      imagePullSecrets:
        - name: docker-creds

---

apiVersion: v1
kind: Service
metadata:
  name: example-webapp-bff-service
spec:
  selector:
    app: example-webapp-bff
  ports:
    - protocol: TCP
      targetPort: 9874
      port: 9874

---

    apiVersion: extensions/v1beta1
    kind: Ingress
    metadata:
      annotations:
        kubernetes.io/ingress.class: "nginx"
        nginx.ingress.kubernetes.io/proxy-read-timeout: "12h"
        nginx.ingress.kubernetes.io/ssl-redirect: "true"
        nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
      name: webapp-ingress
      namespace: default
    spec:
      tls:
        - hosts:
            - example.com
          secretName: example-tls
      rules:
        - host: example.com
          http:
            paths:
              - path: /
                backend:
                  serviceName: webapp-frontend-service
                  servicePort: 80
        - host: example.com
          http:
            paths:
              - path: /api/
                backend:
                  serviceName: webapp-bff-service
                  servicePort: 9874

Spring Boot Config

server.forward-headers-strategy=NATIVE
server.tomcat.remote-ip-header=x-forwarded-for
server.tomcat.internal-proxies="\
              10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|\
              192\\.168\\.\\d{1,3}\\.\\d{1,3}|\
              169\\.254\\.\\d{1,3}\\.\\d{1,3}|\
              127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|\
              172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|\
              172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|\
              172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}\
              172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}\
              <digital-ocean-droplet-IP>\
              <digital-ocean-droplet-IP>\
              <digital-ocean-loadbalancer-IP>"

Digital Ocean Kubernetes Config

I have a cluster that has a single pool of two nodes. I have a Digital Ocean load balancer that sits in front of it. Currently running version 1.17.5.,

Anyone able to provide suggestions? Thanks in advance.

-- Justin Johnson
header
ip
kubernetes
nginx
spring-boot

1 Answer

3/10/2021

Spring boot contains a filter to integrate with reverse proxies out of the box and sets the remote address on the request appropriately. You may need to configure the allowed IPs to accept the header.

Here is an example:

server:
  forward-headers-strategy: native
  tomcat:
    remoteip:
      remote-ip-header: x-forwarded-for
      #private: 10/8, 192.168/16, 169.254/16, 127/8, 172.16/12
      internal-proxies: "\
              10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|\
              192\\.168\\.\\d{1,3}\\.\\d{1,3}|\
              169\\.254\\.\\d{1,3}\\.\\d{1,3}|\
              127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|\
              172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|\
              172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|\
              172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}"

And here is the documentation: https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-use-behind-a-proxy-server

It is important, though, that the right x-forwarded-for header is received by the spring application. Since you are deploying on kubernetes you are most probably using an Ingress to route external requests to the right kubernetes service. Ingress is most often implemented using nginx or traefik and must be configured to accept the x-forwarded-for header, if a L7 reverse proxy is used as external load balancer and TLS termination, if applicable.

If using nginx create a config map to configure nginx

use-forwarded-headers: "true"
proxy-real-ip-cidr: "10.0.0.0/8,..."

When TLS is not terminated externally the external load balancer will not be able to add headers due to the encryption (L3 load balancer). In that case the only option is to use the PROXY protocol which adds meta data to the forwarded tcp stream containing the real remote address.

Example configuration for nginx

use-proxy-protocol: 'true'

Note that there some issues when using proxy protocol and cert-manager to manage TLS certificates with external (kubernetes) LoadBalancer configurations. (See https://github.com/kubernetes/kubernetes/issues/66607 and https://github.com/kubernetes/ingress-nginx/issues/3996 )

Once the setup is correct in all places, the remote ip should be set correctly.

When debugging this kind of setup I recommend to begin on the most outer layer by intercepting (ngrep, tcpdump) or logging requests and move forward step by step.

-- Thomas
Source: StackOverflow