Apache Real IP Logging with Cloudflare
This article is a translation of the following my article:
Original: CloudflareでApacheログに実IPを記録する
* Translated automatically by Google.
* Please note that some links or referenced content in this article may be in Japanese.
* Comments in the code are basically in Japanese.
by bokumin
Apache Real IP Logging with Cloudflare
Introduction
When you enable proxy on your Cloudflare record, Cloudflare acts as a reverse proxy. As a result, web server logs such as Apache only record Cloudflare’s IP address, making fail2ban and other access controls difficult.
Although it is possible to set a block on Cloudflare, in this article we will introduce a setting method to record the actual client IP address on the Apache side.
Verification environment
The versions of OpenSUSE and Apache are as follows.
uname -a
Linux 6.17.0-2-default #1 SMP PREEMPT_DYNAMIC Thu Oct 2 08:12:40 UTC 2025 (190326b) x86_64 x86_64 x86_64 GNU/Linux
sudo apachectl -v
[sudo] password for root:
Server version: Apache/2.4.65 (Linux/SUSE)
Server built: 2025-09-23 13:42:03.000000000 +0000
Setup steps
First, enable the mod_remoteip module.
sudo a2enmod remoteip
sudo systemctl restart apache2
sudo apachectl -M | grep remoteip
remoteip_module (shared)
Create a configuration file for cloudflare in/etc/apache2/conf.d/.
- CF-Connecting-IP → Header sent by Cloudflare to capture the actual client IP address
- RemoteIPTrustedProxy → IP address range to register as a trusted proxy
# cat /etc/apache2/conf.d/cloudflare.conf
<IfModule remoteip_module>
RemoteIPHeader CF-Connecting-IP
RemoteIPTrustedProxy 173.245.48.0/20
RemoteIPTrustedProxy 103.21.244.0/22
RemoteIPTrustedProxy 103.22.200.0/22
RemoteIPTrustedProxy 103.31.4.0/22
RemoteIPTrustedProxy 141.101.64.0/18
RemoteIPTrustedProxy 108.162.192.0/18
RemoteIPTrustedProxy 190.93.240.0/20
RemoteIPTrustedProxy 188.114.96.0/20
RemoteIPTrustedProxy 197.234.240.0/22
RemoteIPTrustedProxy 198.41.128.0/17
RemoteIPTrustedProxy 162.158.0.0/15
RemoteIPTrustedProxy 104.16.0.0/13
RemoteIPTrustedProxy 104.24.0.0/14
RemoteIPTrustedProxy 172.64.0.0/13
RemoteIPTrustedProxy 131.0.72.0/22
</IfModule>
Let’s modify mod_log_config.conf. I think the default is %h, so change it to %a.
- %h → remote hostname (or reverse hostname)
- %a → Client IP address (actual IP when using mod_remoteip)
cat /etc/apache2/mod_log_config.conf
#
# The following directives define some format nicknames for use with
# a CustomLog directive.
#
# https://httpd.apache.org/docs/2.4/mod/mod_log_config.html
#
#
# Format string: Nickname:
#
LogFormat "%a %l %u %t \"%r\" %>s %b" common
LogFormat "%v %a %l %u %t \"%r\" %>s %b" vhost_common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
LogFormat "%a %l %u %t \"%r\" %>s %b \
\"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%v %a %l %u %t \"%r\" %>s %b \
\"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
# To use %I and %O, you need to enable mod_logio
<IfModule mod_logio.c>
LogFormat "%h %a %u %t \"%r\" %>s %b \
\"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
</IfModule>
# Use one of these when you want a compact non-error SSL logfile on a virtual
# host basis:`
<IfModule mod_ssl.c>
Logformat "%t %a %{SSL_PROTOCOL}x %{SSL_CIPHER}x \
\"%r\" %b" ssl_common
Logformat "%t %a %{SSL_PROTOCOL}x %{SSL_CIPHER}x \
\"%r\" %b \"%{Referer}i\" \"%{User-Agent}i\"" ssl_combined
</IfModule>
After changing the settings, reload Apache.
Check the log and if the actual client IP address is recorded, it is successful.
tail /var/log/apache2/access.log
xx.xx.x.xx - - [10/Nov/2025:11:21:57 +0900] "GET /robots.txt HTTP/1.1" 301 238 "-" "Mozilla/5.0 (compatible; MJ12bot/v1.4.8; http://mj12bot.com/)"
xx.xx.x.xx - - [10/Nov/2025:11:22:04 +0900] "GET /art-works/index.php?/search/psk-20250806-5eNDVvVRuC/created-weekly-list HTTP/1.1" 301 299 "-" "Mozilla/5.0 (compatible; MJ12bot/v1.4.8; http://mj12bot.com/)"
xx.xxx.xxx.xxx - - [10/Nov/2025:11:25:50 +0900] "GET /blog/2024/10/17/%E3%83%A1%E3%83%87%E3%82%A3%E3%82%A2%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%EF%BC%88jellyfin%EF%BC%89%E3%82%92linux%E4%B8%8A%E3%81%A7%E6%A7%8B%E7%AF%89%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95/ HTTP/1.1" 301 429 "-" "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/116.0.1938.76 Safari/537.36"
xx.xxx.xxx.xxx - - [10/Nov/2025:11:27:01 +0900] "GET /art-works/picture.php?%2F6%2Ftags%2F3-sleepy%2F6-wallpaper%2F8-illust%2Fposted-monthly-list-2024-10-17 HTTP/1.1" 301 330 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Brave Chrome/85.0.4183.83 Safari/537.36"
Since Cloudflare’s IP ranges change regularly, it is useful to create an automatic update script.
cat /root/bin/add_cloudflare.sh
#!/bin/bash
# /usr/local/bin/update-cloudflare-ips.sh
CF_CONF="/etc/apache2/conf.d/cloudflare.conf"
IPV4=$(curl -s https://www.cloudflare.com/ips-v4)
cat > $CF_CONF << 'EOF'
<IfModule remoteip_module>
RemoteIPHeader CF-Connecting-IP
EOF
for ip in $IPV4; do
echo " RemoteIPTrustedProxy $ip" >> $CF_CONF
done
echo "</IfModule>" >> $CF_CONF
apachectl configtest && systemctl reload apache2
Tomoko:/etc/apache2 #
If you set this script to run with cron about once a month, you can always maintain the latest Cloudflare IP range.
The above is a method to record the real IP in Apache logs even via Cloudflare.
End
Referenced article:
https://developers.cloudflare.com/fundamentals/reference/http-headers/
https://zenn.dev/muchoco/articles/7ea510e0c285a7