-
-
Save mudge/e5c72d701d7fcbda67608deb800193cb to your computer and use it in GitHub Desktop.
Rails.application.configure do | |
# Add Cloudflare's IPs to the trusted proxy list so they are ignored when | |
# determining the true client IP. | |
# | |
# See https://www.cloudflare.com/ips-v4/ and https://www.cloudflare.com/ips-v6/ | |
config.action_dispatch.trusted_proxies = ActionDispatch::RemoteIp::TRUSTED_PROXIES + %w[ | |
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 | |
2400:cb00::/32 | |
2606:4700::/32 | |
2803:f800::/32 | |
2405:b500::/32 | |
2405:8100::/32 | |
2a06:98c0::/29 | |
2c0f:f248::/32 | |
].map { |proxy| IPAddr.new(proxy) } | |
end |
Rack::Attack.throttle("req/ip", limit: 300, period: 5.minutes) do |req| | |
req.get_header("action_dispatch.remote_ip") unless req.path.start_with?("/assets") | |
end |
Thanks for sharing, @trevorturk.
I wanted to configure the default request.remote_ip
to pick the "real" client IP so that it would be reflected throughout the entire Rails app, e.g. in the default logs:
Started GET "/foo/bar" for 1.2.3.4 at 2023-10-19 18:09:16 +0000
^^^^^^^
It's only a little awkward with Rack::Attack because you don't get a ActionDispatch::Request
but a Rack::Request
instead. I did consider the following:
Rack::Attack.throttle("req/ip", limit: 300, period: 5.minutes) do |req|
ActionDispatch::Request.new(req.env).remote_ip unless req.path.start_with?("/assets")
end
But felt instantiating a whole ActionDispatch::Request
might be a bit overkill when what we want is in the action_dispatch.remote_ip
header.
Ah, I hadn't thought of the logs -- nice!
We use the cloudflare-rails gem successfully in production. It fetches Cloudflare's IPs dynamically from Cloudflare.
Thanks, @tmak. It looks like that gem works by overriding the trusted_proxy?
method in Rack::Request::Helpers
and the proxies
method of ActionDispatch::RemoteIp
(see https://github.com/modosc/cloudflare-rails/blob/main/lib/cloudflare/rails/railtie.rb).
It’s quite nice that it fetches the latest IPs from Cloufflare but I wasn’t sure about the stability of patching internal methods on classes (especially as it uses ObjectSpace
to iterate over all loaded classes) vs using a public configuration API. Perhaps a good middle-ground would be to fetch the latest IPs from Cloudflare on application boot but still use Rails.configuration.action_dispatch.trusted_proxies
to get the behaviour we want.
Yes, I feel you.
It uses monkey patching so that your Rails app doesn't have to restart if Cloudflare's IP addresses change.
There are certainly pros and cons to both approaches.
In practice, if you’re running on Heroku, your app will be restarted daily so that gives you some upper limit of how long your IPs will be potentially outdated if Cloudflare does make a change (I have no sense how often they update their IP ranges).
True. We're actually running on Heroku, at the moment 😅
The gem's test suite looked solid to me to use a gem, and monkey patching, rather than having to manage this functionality on our own.
Talking about Cloudflare, Heroku, and Rack::Attack. We use this Rack::Attack configuration to prevent skipping Cloudflare in production. May be helpful for you or others who get here.
class Rack::Attack
if %w[1 true True enabled yes].include?(ENV["REJECT_UNPROXIED_REQUESTS"])
blocklist("block non-proxied requests in production") do |request|
raw_ip = request.get_header("HTTP_X_FORWARDED_FOR")
ip_addresses = raw_ip ? raw_ip.strip.split(/[,\s]+/) : []
proxy_ip = ip_addresses.last
if ::Rails.application.config.cloudflare.ips.any? { |proxy|
proxy === proxy_ip
}
false
else
::Rails.logger.warn "Rack Attack IP Filtering: blocked request from #{proxy_ip} to #{request.url}"
true
end
end
end
end
Ah yes, I do something similar via:
blocklist("herokuapp") do |req|
req.host =~ /herokuapp/
end
...which works for my case, simply enforcing access through my domain as opposed to Heroku directly.
Thanks for sharing!
Just wanted to chime in (and apologies I can't remember where I got this from) but I've been using something like the following with (I believe) pretty good results: