12

My app structure uses GKE and CloudFlare. It looks like this:

CloudFlare -> GKE -> Ingress -> My app running nginx

I'm using the flexible SSL in CloudFlare, so only the connection between the user and CloudFlare uses HTTPS, all the remaining uses HTTP. I know CloudFlare sets the X-Forwarded-Proto to https in this situation, but when I see the headers my nginx app is receiving, it gets X-Forwarded-Proto: http.

I'm pretty sure this happens somewhere between GKE's Load Balancer and Ingress, as I can see that the CF-Visitor: {"scheme": "https"} header configured by CloudFlare is set to HTTPS. My understanding is that this means that CloudFlare did set X-Forwarded-Proto to https, but it got overwritten along the way.

Unfortunately, I couldn't get the header logs from the GKE Load Balancer (it seems they don't log the X-Forwarded-* headers at all), so I can't confirm 100% that CloudFlare is actually setting the headers, but I'd be pretty surprised if it isn't.

If that's true, Google Cloud is overwriting the X-Forwarded-Proto header with http. How can I avoid it doing so?

Edit: I have configured an nginx ingress instead of gce following https://cloud.google.com/community/tutorials/nginx-ingress-gke, and the X-Forwarded-Proto is set to https as expected. This is another signal that it's the gce ingress controller that is overwriting the X-Forwarded-Proto header.

3
  • 1
    It's quite normal to overwrite X-Forwaded-* headers from random untrusted clients. Yes, there should be a method to set trusted clients/proxies, good question.
    – kubanczyk
    May 14, 2018 at 19:25
  • Check to see if you have more than one header value. Well-written proxies and load balancers should insert their value and not overwrite giving you a chain of values. I have not verified this will GCP however. Oct 7, 2019 at 0:16
  • Vitor, did you ever figure out how to convince the GLBs to not strip this header?
    – hornairs
    Dec 6, 2021 at 19:54

3 Answers 3

1

As described in this article Cloudflare appends an X-Forwarded-Proto header which can either be HTTP or HTTPS depending on the protocol the user used to visit the site. If you believe the value of X-Forwarded-Proto should be maintained but was changed by GCLB, I'd recommend opening a feature request for this on Google issue tracker.

0

You can create a middleware:

# frozen_string_literal: true

require 'json'

class CloudflareProxy
  def initialize(app)
    @app = app
  end

  def call(env)
    return @app.call(env) unless env['HTTP_CF_VISITOR']

    env['HTTP_X_FORWARDED_PROTO'] = JSON.parse(env['HTTP_CF_VISITOR'])['scheme']
    @app.call(env)
  end
end

Use in config/application.rb:

config.middleware.use CloudflareProxy

A reference for CF-Visitor header:

https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-

CF-Visitor

A JSON object containing only one key called scheme. The value is identical to that of X-Forwarded-Proto (either HTTP or HTTPS). CF-Visitor is only relevant if using Flexible SSL.

0

Struggled with this on ingress-nginx too. After a couple of days of reading GitHub issues and GCP docs figured out the right values for helm chart:

controller:
  ingressClassResource:
    default: "true"
  service:
    loadBalancerIP: ${var.lb_ip}
    externalTrafficPolicy: Local
  config:
    enable-real-ip: "true"
    compute-full-forwarded-for: "true"
    use-forwarded-headers: "true"
    hsts: "false" # handled by cloudflare, we use flexible encryption mode 
    proxy-real-ip-cidr: "130.211.0.0/22,35.191.0.0/16,173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22"

And on the application level, you should use the second IP from the end in x-forwarded-for header. ExpressJS example:

const express = require('express');
const app = express();
// more on this here: https://expressjs.com/en/guide/behind-proxies.html
app.set('trust proxy', 2);
app.get('/', (req, res) => res.end(`
  ip: ${req.ip}
  headers: ${JSON.stringify(req.headers, null, 2)}
`));
app.listen(80, () => { console.log(`Server running at http://localhost:80`); });

In such case even when someone will try to pass fake ip via headers (or use VPN) your app will use the correct IP:

curl \
  --header "X-Forwarded-For: <FAKE_IP>" \
  --header "X-Forwarded-Proto: https" \
  --header "X-Forwarded-Host: example.com" https://example.com

  ip: <REAL_IP>
  headers: {
  "host": "example.com",
  "x-request-id": "...",
  "x-real-ip": "<REAL_IP>",
  "x-forwarded-for": "<FAKE_IP>,<REAL_IP>, <CLOUDFLARE_IP>",
  "x-forwarded-host": "example.com",
  "x-forwarded-port": "80",
  "x-forwarded-proto": "https",
  "x-forwarded-scheme": "https",
  "x-scheme": "https",
  "x-original-forwarded-for": "<FAKE_IP>,<REAL_IP>",
  "accept-encoding": "gzip",
  "cf-ray": "...",
  "cf-visitor": "{\"scheme\":\"https\"}",
  "user-agent": "curl/8.0.1",
  "accept": "*/*",
  "cf-connecting-ip": "<REAL_IP>",
  "cf-ipcountry": "...",
  "cdn-loop": "cloudflare"
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.