Перейти к основному содержимому

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. Конфигурация туннеля​

/opt/xlada/cloudflared/config.yml
# 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-For Cloudflare дополняет, а не заменяет: если клиент прислал свой заголовок, в цепочке первыми окажутся подставленные им значения, а реальный адрес — последним. Опираться на левое значение нельзя;
  • cloudflared передаёт запрос в сервис originRequest как есть и не умеет принудительно выставлять X-Forwarded-Proto: https. Если посетитель пришёл по http, панель такой запрос отклонит.

Поэтому надёжный вариант — поставить перед панелью небольшой прокси, который сам выставит заголовки. Ниже — полный конфиг обоих локальных origin-серверов: панели на 127.0.0.1:8080 и страницы подписки на 127.0.0.1:8081.

/etc/nginx/sites-available/xlada-origin
# Локальные 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. Запуск контейнером​

/opt/xlada/cloudflared/docker-compose.yml
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 отличается только путём к ключам туннеля. Полный файл для этого способа:

/opt/xlada/cloudflared/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 выглядит так:
/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 и то, что локальный прокси слушает нужный порт.

Что дальше​