Сервер: Реальный IP клиента за Cloudflare в nginx

Когда DNS-запись проксируется через Cloudflare, nginx видит IP-адрес узла Cloudflare, а не посетителя. Из-за этого журналы, ограничения запросов, аналитика и приложение могут считать разных пользователей одним клиентом.

Правильное решение — включить модуль ngx_http_realip_module, доверять только официальным сетям Cloudflare и брать адрес посетителя из заголовка CF-Connecting-IP. После настройки реальный адрес будет доступен в nginx как $remote_addr, а в PHP — как $_SERVER['REMOTE_ADDR'].

Когда решение не подходит: инструкция рассчитана на схему, в которой Cloudflare напрямую подключается к nginx. Если перед nginx стоит ещё один балансировщик, reverse proxy, контейнерный ingress или Cloudflare Tunnel, доверять нужно адресу непосредственного прокси, а конфигурацию следует адаптировать под всю цепочку.

Что было неверно в старом варианте

  • X-Forwarded-For может содержать цепочку адресов и значения, добавленные предыдущими прокси. Для обычной схемы Cloudflare → nginx надёжнее использовать CF-Connecting-IP, содержащий один IP-адрес.
  • Нельзя доверять заголовку от любого отправителя. Иначе посетитель сможет подставить произвольный IP.
  • Команда curl -I https://example.com не обязана показывать CF-Connecting-IP: Cloudflare отправляет этот заголовок в запросе к origin-серверу, а не в ответе браузеру.
  • Диапазоны Cloudflare меняются, поэтому конфигурацию нужно периодически сверять с официальным источником.

1. Проверить модуль realip

nginx -V 2>&1 | grep -- '--with-http_realip_module'

Если команда вывела --with-http_realip_module, модуль доступен. Если вывода нет, установите пакет nginx со встроенным модулем или пересоберите nginx с параметром:

--with-http_realip_module

2. Готовая конфигурация nginx

Создайте файл /etc/nginx/conf.d/cloudflare-realip.conf. Обычно каталог conf.d подключён внутри блока http в /etc/nginx/nginx.conf.

# /etc/nginx/conf.d/cloudflare-realip.conf
#
# Источник диапазонов:
# https://www.cloudflare.com/ips-v4
# https://www.cloudflare.com/ips-v6
#
# Проверено: 18.06.2026

# IPv4
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;

# IPv6
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;

# Cloudflare передаёт исходный адрес посетителя в одном значении.
real_ip_header CF-Connecting-IP;

Директива real_ip_recursive on здесь не требуется: CF-Connecting-IP содержит один адрес, а не цепочку прокси. Она нужна в других схемах, например при разборе корректно сформированного X-Forwarded-For через несколько доверенных прокси.

3. Проверить конфигурацию и перечитать nginx

nginx -t && systemctl reload nginx

Используйте reload, а не безусловный restart: при успешной проверке nginx перечитает конфигурацию без обрыва активных соединений.

4. Проверить реальный IP через журнал nginx

Временно добавьте в контекст http отдельный формат журнала:

log_format realIpDebug
	'client=$remote_addr '
	'cloudflare=$realip_remote_addr '
	'header=$http_cf_connecting_ip '
	'ray=$http_cf_ray '
	'request="$request"';

access_log /var/log/nginx/real-ip-debug.log realIpDebug;

Проверьте конфигурацию, перечитайте nginx и откройте сайт через проксируемый домен:

nginx -t && systemctl reload nginx
tail -f /var/log/nginx/real-ip-debug.log

Ожидаемый результат:

client=203.0.113.10 cloudflare=172.70.250.10 header=203.0.113.10 ray=... request="GET / HTTP/2.0"
  • $remote_addr — восстановленный адрес посетителя.
  • $realip_remote_addr — адрес узла Cloudflare, который подключился к nginx.
  • $http_cf_connecting_ip — исходное значение заголовка.
  • $http_cf_ray — идентификатор запроса Cloudflare, полезный для диагностики.

После проверки удалите отладочный журнал, если он больше не нужен.

Проверка в PHP

После обработки заголовка на уровне nginx приложение должно использовать REMOTE_ADDR, а не читать HTTP_CF_CONNECTING_IP напрямую.

<?php

declare(strict_types=1);

header('Content-Type: text/plain; charset=utf-8');

$clientIp = $_SERVER['REMOTE_ADDR'] ?? '';

if (filter_var($clientIp, FILTER_VALIDATE_IP) === false)
{
	http_response_code(500);
	echo 'IP-адрес недоступен';
	exit;
}

echo $clientIp;

Откройте тестовый файл через домен с включённым проксированием Cloudflare, сравните результат со своим внешним IP и затем удалите файл.

Автоматическое обновление диапазонов Cloudflare

Статический список со временем может устареть. Следующий скрипт загружает официальные IPv4- и IPv6-диапазоны, проверяет их через Python, атомарно создаёт конфигурацию, запускает nginx -t и откатывает изменения при ошибке.

#!/usr/bin/env bash

set -Eeuo pipefail

if (( EUID != 0 ))
then
	echo 'Запустите скрипт от root' >&2
	exit 1
fi

configFile='/etc/nginx/conf.d/cloudflare-realip.conf'
workDir="$(mktemp -d)"
ipv4File="${workDir}/ips-v4"
ipv6File="${workDir}/ips-v6"
newConfig="${workDir}/cloudflare-realip.conf"
backupFile="${workDir}/cloudflare-realip.conf.backup"
hadConfig=0

trap 'rm -rf "$workDir"' EXIT

if ! nginx -V 2>&1 | grep -q -- '--with-http_realip_module'
then
	echo 'nginx собран без ngx_http_realip_module' >&2
	exit 1
fi

curl \
	--fail \
	--silent \
	--show-error \
	--location \
	--proto '=https' \
	--tlsv1.2 \
	'https://www.cloudflare.com/ips-v4' \
	--output "$ipv4File"

curl \
	--fail \
	--silent \
	--show-error \
	--location \
	--proto '=https' \
	--tlsv1.2 \
	'https://www.cloudflare.com/ips-v6' \
	--output "$ipv6File"

python3 - "$ipv4File" "$ipv6File" <<'PY'
import ipaddress
import pathlib
import sys

for fileName in sys.argv[1:]:
	path = pathlib.Path(fileName)
	ranges = [
		line.strip()
		for line in path.read_text(encoding='utf-8').splitlines()
		if line.strip()
	]

	if not ranges:
		raise SystemExit(f'Пустой список диапазонов: {path}')

	for network in ranges:
		ipaddress.ip_network(network, strict=True)
PY

{
	echo '# Generated automatically from official Cloudflare ranges'
	echo '# https://www.cloudflare.com/ips-v4'
	echo '# https://www.cloudflare.com/ips-v6'
	echo

	while IFS= read -r network
	do
		 && printf 'set_real_ip_from %s;\n' "$network"
	done < "$ipv4File"

	echo

	while IFS= read -r network
	do
		 && printf 'set_real_ip_from %s;\n' "$network"
	done < "$ipv6File"

	echo
	echo 'real_ip_header CF-Connecting-IP;'
} > "$newConfig"

if 
then
	cp -a "$configFile" "$backupFile"
	hadConfig=1
fi

install -o root -g root -m 0644 "$newConfig" "$configFile"

restoreConfig()
{
	if (( hadConfig == 1 ))
	then
		cp -a "$backupFile" "$configFile"
	else
		rm -f "$configFile"
	fi
}

if ! nginx -t
then
	echo 'Новая конфигурация nginx не прошла проверку. Выполняется откат.' >&2
	restoreConfig
	nginx -t || true
	exit 1
fi

if ! systemctl reload nginx
then
	echo 'nginx не смог перечитать конфигурацию. Выполняется откат.' >&2
	restoreConfig
	nginx -t && systemctl reload nginx || true
	exit 1
fi

echo 'Диапазоны Cloudflare обновлены, nginx перечитал конфигурацию.'

Сохраните скрипт, например, как /usr/local/sbin/update-cloudflare-realip:

chmod 750 /usr/local/sbin/update-cloudflare-realip
/usr/local/sbin/update-cloudflare-realip

Автоматический запуск через systemd timer

Создайте сервис /etc/systemd/system/cloudflare-realip-update.service:

[Unit]
Description=Update Cloudflare IP ranges for nginx
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/update-cloudflare-realip

Создайте таймер /etc/systemd/system/cloudflare-realip-update.timer:

[Unit]
Description=Weekly update of Cloudflare IP ranges

[Timer]
OnCalendar=Sun *-*-* 04:20:00
RandomizedDelaySec=30m
Persistent=true

[Install]
WantedBy=timers.target

Включите таймер:

systemctl daemon-reload
systemctl enable --now cloudflare-realip-update.timer
systemctl list-timers cloudflare-realip-update.timer

Безопасность

  • Никогда не используйте set_real_ip_from 0.0.0.0/0 или set_real_ip_from ::/0. Это позволит любому клиенту подменить адрес через HTTP-заголовок.
  • Не читайте CF-Connecting-IP напрямую в PHP без проверки источника запроса. Настройте доверенные сети в nginx и используйте REMOTE_ADDR.
  • Закройте прямой доступ к origin-серверу на порты 80 и 443, разрешив Cloudflare и необходимые служебные адреса. Иначе злоумышленник сможет обходить WAF, кеш и ограничения Cloudflare.
  • Перед ограничением firewall учтите SSH, мониторинг, балансировщики, резервные системы и проверку сертификатов. Ошибка в allowlist может сделать сайт недоступным и вызвать Cloudflare 521/522.
  • Для более строгой защиты origin рассмотрите Authenticated Origin Pulls или Cloudflare Tunnel.

Краевые случаи

  • Cloudflare Tunnel: nginx обычно получает соединение от локального cloudflared. Доверять нужно только локальному адресу или Unix-сокету, через который приходит трафик, а не всем сетям Cloudflare.
  • Дополнительный reverse proxy: в set_real_ip_from укажите адрес непосредственного доверенного прокси. Он должен передавать CF-Connecting-IP без возможности подмены.
  • Pseudo IPv4: при режиме перезаписи заголовков IPv6 посетителя может быть заменён псевдо-IPv4. Исходный IPv6 будет доступен в CF-Connecting-IPv6.
  • Отключённый proxy: если DNS-запись в Cloudflare имеет серое облако, запрос пойдёт прямо на сервер и заголовка CF-Connecting-IP не будет.
  • Удаление visitor IP headers: проверьте Managed Transforms в Cloudflare, если нужный заголовок не приходит на origin.
  • Firewall и fail2ban: сетевой firewall видит IP Cloudflare, а не восстановленный адрес посетителя. Блокировку посетителя выполняйте через Cloudflare WAF/API либо на уровне nginx после обработки realip.

Производительность

Модуль realip выполняет простую проверку адреса соединения по доверенным CIDR-диапазонам и не создаёт заметной нагрузки. После восстановления $remote_addr его можно использовать в limit_req, limit_conn, geo, журналах и приложении.

Совместимость

  • nginx с модулем ngx_http_realip_module;
  • AlmaLinux 8/9, Rocky Linux, RHEL, Ubuntu и другие Linux-системы;
  • IPv4 и IPv6;
  • обычное проксирование Cloudflare с оранжевым облаком.

Пути к конфигурации и имя systemd-сервиса nginx могут отличаться в нестандартной сборке или контейнере.

Итоговая проверка

  1. DNS-запись домена проксируется через Cloudflare.
  2. nginx собран с ngx_http_realip_module.
  3. В set_real_ip_from находятся только официальные сети Cloudflare.
  4. Используется real_ip_header CF-Connecting-IP.
  5. Команда nginx -t завершается успешно.
  6. В журнале $remote_addr содержит адрес посетителя, а $realip_remote_addr — адрес Cloudflare.
  7. PHP получает адрес из $_SERVER['REMOTE_ADDR'].
  8. Прямой доступ к origin ограничен или дополнительно защищён.

Альтернативы

  • Cloudflare Tunnel: origin не требует публичного входящего подключения.
  • Authenticated Origin Pulls: nginx проверяет клиентский сертификат Cloudflare.
  • PROXY protocol: применяется в других балансировщиках и L4-схемах, но требует отдельной настройки listen ... proxy_protocol.
  • mod_remoteip: аналогичная настройка для Apache.

Документация

up: Просмотров: 2.3k
Материал помог? Оценка помогает находить устаревшие и спорные решения.
Оценки до обновления материала
  • Полезно1

Комментарии

0

Нашли ошибку или хотите дополнить материал?

Комментариев пока нет.Можно первым дополнить материал или сообщить об ошибке.
Telegram