Cloudflare Tunnel
Cloudflare Tunnel (демон cloudflared) создаёт исходящее соединение с сетью
Cloudflare и публикует ваш сервис без белого IP и без открытых портов 80 и
443. На хосте не нужны ни reverse proxy с сертификатом, ни публичный адрес:
трафик приходит от Cloudflare по уже установленному туннелю, а TLS терминируется
на стороне Cloudflare.
1. Когда туннель уместен
Туннель подходит, если:
- у сервера нет статического белого IP или провайдер блокирует входящие порты;
- нужно скрыть адрес origin от сканирования;
- вы уже используете Cloudflare для DNS и фильтрации атак.
Туннель не подходит, если:
- панель должна работать без зависимости от стороннего сервиса;
- вы не готовы к тому, что Cloudflare видит весь трафик в открытом виде;
- нужен стабильный SSH-терминал к нодам: туннель обрывает простаивающие WebSocket-соединения заметно агрессивнее, чем прямой прокси.
Схема трафика выглядит так: браузер обращается к Cloudflare по HTTPS, edge
Cloudflare передаёт запрос по уже открытому туннелю на хост, а cloudflared
отдаёт его локальному сервису по HTTP. На хосте не нужен ни сертификат, ни
открытый порт — наружу смотрит только исходящее соединение демона.
Quick Tunnel (адрес вида *.trycloudflare.com) годится только для теста: у
него лимит одновременных соединений, случайный домен и нет гарантий
доступности. Для продакшена используйте именованный туннель и свой домен.
2. Установка cloudflared
Демон не требует входящих портов: он сам устанавливает исходящие соединения с
ближайшим узлом Cloudflare. В firewall достаточно разрешить исходящие
соединения на порт 7844 (UDP для протокола quic, TCP для http2); порт
443 нужен только для проверки обновлений. Если провайдер блокирует UDP,
добавьте в команду запуска флаг --protocol http2.
mkdir -p /opt/xlada/cloudflared && cd /opt/xlada/cloudflared
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
-o /usr/share/keyrings/cloudflare-main.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] \
https://pkg.cloudflare.com/cloudflared any main" \
> /etc/apt/sources.list.d/cloudflared.list
apt update && apt install -y cloudflared
cloudflared --version
3. Авторизация и создание туннеля
# Откроется ссылка: подтвердите доступ к домену в браузере
cloudflared tunnel login
# Создаём именованный туннель. JSON-файл ключей появится в ~/.cloudflared/
cloudflared tunnel create xlada
# Смотрим UUID — он понадобится в config.yml
cloudflared tunnel list
4. DNS-записи
cloudflared tunnel route dns xlada panel.example.com
cloudflared tunnel route dns xlada sub.example.com
Команды создают CNAME-записи на адрес туннеля, вручную ничего добавлять не нужно. Проксирование (оранжевое облако) включается автоматически. Режим SSL/TLS выставьте в Full или Full (strict) и включите Always Use HTTPS, чтобы посетитель не мог прийти на панель по http.
5. Конфигурация туннеля
# UUID из вывода `cloudflared tunnel list`
tunnel: 00000000-0000-0000-0000-000000000000
# JSON-файл ключей, созданный командой cloudflared tunnel create
credentials-file: /root/.cloudflared/00000000-0000-0000-0000-000000000000.json
# Логи и локальная страница метрик туннеля
loglevel: info
logfile: /var/log/cloudflared.log
metrics: 127.0.0.1:49312
ingress:
# Страница подписки: трафик идёт через локальный прокси на 8081
- hostname: sub.example.com
service: http://127.0.0.1:8081
originRequest:
# Сохраняем исходный Host, иначе приложение увидит 127.0.0.1
httpHostHeader: sub.example.com
# Панель: локальный прокси на 8080 из раздела 6
- hostname: panel.example.com
service: http://127.0.0.1:8080
originRequest:
httpHostHeader: panel.example.com
connectTimeout: 30s
# Обязательное правило-заглушка: всё, что не совпало
- service: http_status:404
Оба сервиса указывают на локальный прокси, а не на порты 3000 и 3010
напрямую. Так заголовки всегда формирует один и тот же узел, и панель получает
предсказуемые значения независимо от поведения cloudflared.
Проверьте конфигурацию до запуска — команда укажет на опечатки в правилах:
cloudflared tunnel ingress validate
cloudflared tunnel ingress rule https://panel.example.com/
6. Заголовки и локальный прокси
Панель и страница подписки разрывают соединение, если запрос пришёл без
X-Forwarded-For и X-Forwarded-Proto: https. Разберитесь, как эти заголовки
ведут себя в туннеле:
- Cloudflare принимает запрос и перезаписывает
X-Forwarded-Protoзначением схемы, по которой пришёл посетитель. При включённом Always Use HTTPS это всегдаhttps; X-Forwarded-ForCloudflare дополняет, а не заменяет: если клиент прислал свой заголовок, в цепочке первыми окажутся подставленные им значения, а реальный адрес — последним. Опираться на левое значение нельзя;cloudflaredпередаёт запрос в сервисoriginRequestкак есть и не умеет принудительно выставлятьX-Forwarded-Proto: https. Если посетитель пришёл по http, панель такой запрос отклонит.
Поэтому надёжный вариант — поставить перед панелью небольшой прокси, который
сам выставит заголовки. Ниже — полный конфиг обоих локальных origin-серверов:
панели на 127.0.0.1:8080 и страницы подписки на 127.0.0.1:8081.
# Локальные origin-серверы для Cloudflare Tunnel.
map $http_upgrade $cf_connection_upgrade {
default upgrade;
'' close;
}
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
application/javascript
application/json
application/manifest+json
image/svg+xml
text/css
text/javascript
text/plain
text/xml;
# ─── Панель XLADA: сюда cloudflared отдаёт panel.example.com ─────────
server {
listen 127.0.0.1:8080;
server_name panel.example.com;
client_max_body_size 32m;
# Статика сборки панели: хеш в имени файла позволяет кэшировать её
location ^~ /assets/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Connection "";
expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
# WebSocket — SSH-терминал к нодам
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $cf_connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Только реальный адрес клиента, без подставленной цепочки
proxy_set_header X-Forwarded-For $remote_addr;
# Ключевая строка: панель ждёт https
proxy_set_header X-Forwarded-Proto https;
proxy_connect_timeout 10s;
proxy_send_timeout 86400s;
proxy_read_timeout 86400s;
}
}
# ─── Страница подписки: сюда cloudflared отдаёт sub.example.com ──────
server {
listen 127.0.0.1:8081;
server_name sub.example.com;
client_max_body_size 10m;
location ^~ /assets/ {
proxy_pass http://127.0.0.1:3010;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Connection "";
expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
location / {
proxy_pass http://127.0.0.1:3010;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
# WebSocket: сейчас не используется, но конфиг готов к нему
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $cf_connection_upgrade;
proxy_connect_timeout 10s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
}
ln -sf /etc/nginx/sites-available/xlada-origin /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
Оба имени сервера совпадают с httpHostHeader в правилах config.yml, поэтому
панель и страница подписки получают исходный домен, а не 127.0.0.1.
7. Запуск контейнером
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
# Туннель идёт наружу, входящие порты не нужны
network_mode: host
command: tunnel --no-autoupdate --config /etc/cloudflared/config.yml run
volumes:
- ./config.yml:/etc/cloudflared/config.yml:ro
- ./credentials.json:/etc/cloudflared/credentials.json:ro
# Скопируйте JSON-файл ключей из ~/.cloudflared/ рядом с compose-файлом
cp /root/.cloudflared/00000000-0000-0000-0000-000000000000.json \
/opt/xlada/cloudflared/credentials.json
docker compose up -d && docker compose logs -f
В контейнере файл config.yml отличается только путём к ключам туннеля.
Полный файл для этого способа:
# UUID из вывода `cloudflared tunnel list`
tunnel: 00000000-0000-0000-0000-000000000000
# Ключи туннеля лежат рядом с docker-compose.yml
credentials-file: /etc/cloudflared/credentials.json
# Логи и локальная страница метрик туннеля
loglevel: info
logfile: /var/log/cloudflared.log
metrics: 127.0.0.1:49312
ingress:
# Страница подписки: локальный прокси на 8081
- hostname: sub.example.com
service: http://127.0.0.1:8081
originRequest:
httpHostHeader: sub.example.com
# Панель: локальный прокси на 8080
- hostname: panel.example.com
service: http://127.0.0.1:8080
originRequest:
httpHostHeader: panel.example.com
connectTimeout: 30s
# Обязательное правило-заглушка: всё, что не совпало
- service: http_status:404
8. Запуск как сервис
mkdir -p /etc/cloudflared
cp config.yml /etc/cloudflared/config.yml
cp /root/.cloudflared/*.json /etc/cloudflared/
cloudflared service install
systemctl enable --now cloudflared
systemctl status cloudflared
9. Дополнительная защита панели
Публичный адрес панели видят все, поэтому одного пароля мало. Два способа закрыть вход:
- Cloudflare Access — политика Zero Trust перед приложением: панель
откроется только после входа в ваш identity provider. Страницу подписки
можно оставить публичной или защитить её отдельно; для неё в
.envпредусмотрены переменныеCLOUDFLARE_ZERO_TRUST_CLIENT_IDиCLOUDFLARE_ZERO_TRUST_CLIENT_SECRET; - WAF-правило с ограничением по стране или по списку адресов, если администраторы работают из известных сетей.
Оба механизма дополняют, а не заменяют настройки самой панели: включите двухфакторную аутентификацию и не отключайте ограничение попыток входа.
10. Ограничения
- Реальный IP клиента. Cloudflare добавляет заголовок
CF-Connecting-IPс адресом клиента; рекомендуемый источник истины — именно он, а неX-Forwarded-For. Если перед панелью стоит nginx, доверяют только диапазонам Cloudflare. Полный файл/etc/nginx/conf.d/real-ip.confвыглядит так:
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;
real_ip_header CF-Connecting-IP;
real_ip_recursive on;
Список диапазонов Cloudflare периодически обновляется — сверяйте его с актуальным.
- WebSocket и SSH-терминал. WebSocket через Cloudflare работает, но простаивающие соединения обрываются. Для терминала к нодам надёжнее подключаться к панели напрямую, а через туннель отдавать страницу подписки.
- Лимиты. У бесплатного тарифа есть ограничения на размер загружаемых файлов и число одновременных соединений; значения периодически меняются.
- SSL/TLS. Режим Flexible приведёт к редиректу по кругу и потере заголовков. Используйте Full или Full (strict).
- Порты. Туннель публикует наружу только стандартные порты Cloudflare
(
443,80и часть альтернативных). Внутренние адреса вроде3010остаются локальными.
11. Проверка
cloudflared tunnel info xlada
cloudflared tunnel ingress validate
curl -sI https://panel.example.com | head -3
curl -s -o /dev/null -w '%{http_code}\n' https://sub.example.com/
journalctl -u cloudflared -n 30
tail -n 30 /var/log/cloudflared.log
Корень панели должен вернуть страницу входа, а не 502 и не ошибку про
заголовки. При 502 проверьте service в config.yml и то, что локальный
прокси слушает нужный порт.