Схема на Cloudflare Tunnel между облака и домашния сървър

Linux — практическо ръководство

Част 9: Cloudflare Tunnel — 6 сайта без един отворен порт

Дълго време home lab означаваше компромис: или държиш сайтовете си само за вътрешна употреба, или отваряш портове на рутера и се молиш никой сканер да не ги намери. Днес публикувам шест сайта от сървъра си вкъщи и рутерът ми няма нито един пренасочен порт. Инструментът, който го прави възможно, е Cloudflare Tunnel.

ℹ Бележка Това е описанието на моето реално setup: Proxmox VE на физическия сървър, отделен LXC контейнер за Docker, а всички публични услуги минават през един cloudflared конектор. Командите са от Debian-базиран LXC. Не е нужен статичен IP адрес — това е една от най-големите ползи от този подход.

1. Проблемът, който тунелът решава

Класическият начин да публикуваш услуга от домашна мрежа изглежда така: на рутера се отваря порт 443, прави се port forwarding към вътрешния сървър и динамичният IP адрес се обвързва с DDNS услуга. Този модел има три проблема, които лично ме притесняваха:

  • Повърхност за атака. Отворен порт означава, че всеки бот в интернет може да го сканира, да опита exploit срещу Nginx, да пипа TLS handshake-а. Дори конфигурацията да е чиста, аз трябва да я поддържам чиста всеки ден.
  • Динамичен IP. Доставчикът ми сменя адреса. DDNS решава част от проблема, но добавя още една движеща се част, която може да се развали тихо.
  • Доверителност на рутера. Поредния ISP рутер е черна кутия. Не знам какво firmware тича, какви уязвимости има и кога ще получи последния си ъпдейт.

С Cloudflare Tunnel посоката на връзката се обръща: сървърът ми излиза навън и поддържа постоянна изходяща връзка към Cloudflare, а посетителите отиват при Cloudflare, не при мен. Рутерът не получава нито едно входящо правило.

2. Как работи — една картинка вместо десет изречения

cloudflared е малък демон, който тича на сървъра ми. При старта си той установява изходяща QUIC връзка към два от closest Cloudflare центровете и я поддържа жива. Когато някой отвори serafiev.com, заявката стига до Cloudflare Edge, през вече установения тунел стига до моя конектор, а конекторът я предава на правилния локален контейнер.

Архитектурна схема: посетител → Cloudflare Edge → изходящ QUIC тунел → cloudflared в Proxmox → шест Docker контейнера. Рутерът няма пренасочени портове.

Три неща си струва да забележите в схемата:

  1. Домейнът сочи към тунела, не към IP. DNS записът за всеки сайт е CNAME към <TUNNEL-ID>.cfargotunnel.com. Нямам A запис с домашния адрес, значи адресът ми не се изтича през DNS.
  2. TLS terminates при Edge. Посетителят говори HTTPS с Cloudflare, а тунелът носи криптиран трафик към мен. Сертификати не управлявам изобщо — Let’s Encrypt и cron jobs за подновяване отпадат от уравнението.
  3. Няма нищо за сканиране. Порт 80 и 443 на рутера са затворени. Ssh-ботовете, които обикалят интернет, виждат… нищо.

3. Какво ми е необходимо

Списъкът е кратък:

  • Домейн, управляван в Cloudflare (free планът е достатъчен — тунелът не е платена функция).
  • Сървър с Linux — в моя случай LXC контейнер с Docker върху Proxmox VE.
  • cloudflared — конекторът, инсталиран на сървъра.

Домашната лаборатория: мини-PC сървър с Docker контейнери, всеки от които обслужва по един сайт зад тунела.

За шестте сайта в моята лаборатория един-единствен конектор е напълно достатъчен — те споделят един cloudflared процес, а разпределянето става по hostname правило в конфигурацията.

4. Инсталирам cloudflared на Debian

Cloudflare поддържа собствен apt repository. Добавям го ръчно:

Добавяне на Cloudflare репозитория
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt-get update && sudo apt-get install cloudflared

Проверявам инсталацията:

Проверка на версията
cloudflared --version

⚠ Предупреждение В LXC контейнер без systemd (unprivileged шаблон без systemd enable) не мога да разчитам на systemctl. Аз тичам cloudflared като Docker контейнер точно заради това — вижте раздел 8. За класическа инсталация на VM или физическа машина обаче deb пакетът с вградената systemd услуга е най-простият път.

5. Свързвам конектора с Cloudflare акаунта

Първо се автентикирам:

Login през браузъра
cloudflared tunnel login

Командата дава URL — отварям го, избирам домейна си и Cloudflare сваля сертификатът в ~/.cloudflared/cert.pem. Този файл оторизира тази машина да създава тунели в акаунта.

После създавам самия тунел и получавам уникален идентификатор:

Създаване на тунел
cloudflared tunnel create homelab

Отговорът изглежда приблизително така:

Created tunnel homelab with id 1a2b3c4d-5e6f-...

Записвам си ID-то — то влиза в DNS CNAME записите и в конфигурацията.

6. Конфигурирам ingress правилата

Сърцето на цялата схема е config.yml. Там описвам кое hostname към кой локален порт се пренасочва. Моят опростен вариант:

~/.cloudflared/config.yml
tunnel: 1a2b3c4d-5e6f-...
credentials-file: /etc/cloudflared/1a2b3c4d-5e6f-....json

ingress:
  - hostname: serafiev.com
    service: http://172.19.0.1:8080
  - hostname: dev.fedia.eu
    service: http://172.19.0.1:8081
  - hostname: status.fedia.eu
    service: http://172.19.0.1:8082
  - service: http_status:404

Три правила, които спазвам:

  1. Последният ред е задължителен. http_status:404 е catch-all — без него cloudflared отказва да тръгне. Всеки хостнейм, който не съм описал, получава 404.
  2. IP адрес вместо localhost. Конекторът тича в контейнер, а сайтовете — в други. Затова соча към bridge интерфейса на Docker мрежата, не към 127.0.0.1.
  3. HTTP локално е ок. Трафикът между мен и Cloudflare е криптиран от самия тунел. В локалната мрежа не ми трябва втори TLS слой — но ако услугата поддържа, може и https:// с originRequest: { noTLSVerify: true }.

DNS записите мога да ги направя и от CLI-то, без да влизам в Dashboard:

DNS CNAME запис през cloudflared
cloudflared tunnel route dns homelab serafiev.com
cloudflared tunnel route dns homelab dev.fedia.eu
cloudflared tunnel route dns homelab status.fedia.eu

Всяка команда създава CNAME <hostname> → <tunnel-id>.cfargotunnel.com.

7. Пускам тунела и го проверявам

Първо стартиране на преден план
cloudflared tunnel run homelab

Гледам логовете: търся редове от вида Registered tunnel connection — това са активните връзки към Edge. После проверявам отвън:

Smoke test
curl -sI https://serafiev.com | head -3

Ако виждам HTTP/2 200 с server: cloudflare, схемата работи. След това остава само да осигуря автоматичен рестарт при рестарт на сървъра.

✓ Успех Моят навик: след всяка промяна в config.yml правя cloudflared tunnel ingress validate. Така хващам синтактични грешки и липсващ catch-all, преди да са счупили всички сайтове наведнъж.

8. Моето решение: cloudflared като Docker контейнер

Понеже всичко останало в лабата ми е контейнеризирано, и конекторът е контейнер. Така ъпгрейдът е docker compose pull, а конфигурацията живее заедно с останалите compose файлове:

docker-compose.yml за cloudflared
services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=${CLOUDFLARED_TOKEN}
    network_mode: host

Токенът взимам от Dashboard-а (Zero Trust → Networks → Tunnels → избрания тунел) и го държам в .env файл до compose файла. При този модел ingress правилата се управляват централно в Dashboard-а, а не в локален config.yml — двата подхода не се смесват, избира си човек единия.

network_mode: host е умишлено: така контейнерът вижда локалната мрежа на хоста и стига до останалите услуги без допълнителни Docker network танци.

9. Какво спечелих — старият начин срещу тунела

КритерийPort forwarding + DDNSCloudflare Tunnel
Отворени портове на рутера80 и/или 4430
Повърхност за атакаВсеки сканер стига до NginxEdge филтрира преди мен
TLS сертификатиСобствени, с подновяванеManaged при Edge
Динамичен IPНужен DDNSБез значение
DDoS защитаНямаВключена в плана
Скриване на реалния IPЧастичноПълен — няма A запис

Последният ред често го подценяват: когато няма A запис, никой не може да разбере къде физически е сървърът. За мен, който държи всички инфраструктурни сайтове вкъщи, това не е параноично — това е хигиена.

10. Сигурност на няколко нива

Тунелът решава мрежовия въпрос, но не заменя останалата дисциплина. При мен:

  • Файрволът на Proxmox пуска към cloudflared LXC-а само изходящ трафик и локалния мрежови диапазон. Входящи правила от интернет — нулев брой.
  • Cloudflare WAF и Bot Fight Mode филтрират смислената част от боклука още при Edge. Два клика в Dashboard-а, безплатно.
  • Cloudflare Access за административните интерфейси: Proxmox UI-то и мониторингът не са публични — преди да стигнат до тях, Cloudflare иска SSO автентикация.
  • Редовни бекъпи, които тичат автоматично — защото най-добрата защита от инцидент си остава възможността да възстановиш.

11. Проблемите, които срещнах

За да не учите същите уроци трудният начин:

  • QUIC е блокиран от някои ISP-и. Ако в логовете се вижда цикъл на reconnect, пробвам --protocol http2 в командата на cloudflared. По-бавен handshake, но стабилен.
  • Разпространение на DNS. След route dns новият hostname може да не отговори минути — не паникьосвам, а проверявам с dig CNAME hostname +short.
  • Забравен catch-all. След добавяне на нов сайт без http_status:404 накрая тунелът не тръгна и шестте сайта паднаха едновременно. Оттогава validate е ритуал.
  • Прекалено много доверие в локалната мрежа. Тунелът не пази от повреден контейнер, който слуша на всички интерфейси. Затова файрволът на Proxmox е второто ниво, а не допълнение.

12. Командите накратко

КомандаЗа какво я използвам
cloudflared tunnel loginОторизирам машината в Cloudflare акаунта.
cloudflared tunnel create NAMEСъздавам тунел и получавам ID.
cloudflared tunnel route dns NAME HOSTСъздавам CNAME запис към тунела.
cloudflared tunnel ingress validateПроверявам config.yml преди рестарт.
cloudflared tunnel run NAMEСтартирам тунела на преден план.
cloudflared tunnel listПоказвам тунелите в акаунта.
cloudflared tunnel info NAMEВиждам статус и активни конектори.
cloudflared tunnel delete NAMEИзтривам тунел.
systemctl status cloudflaredПроверявам услугата при systemd инсталация.
journalctl -u cloudflared -fСледя логовете на живо.

13. Заключение

Cloudflare Tunnel обърна моята логика на публикуване: вместо да отварям вход и да пазя дверите, сървърът ми излиза навън и работи през връзка, която сам той е установил. Шестте сайта в лабораторията ми живеят зад един конектор, един YAML файл и нулата пренасочени порта на рутера.

Не е сребърен куршум — зависиш от Cloudflare и от това, че конекторът е жив. Но за моя масштаб (домашен сървър, няколко услуги, малко време за поддръжка) балансът между сигурност, цена и простота е непобедим.

✓ Успех Моят принцип: най-безопасният отворен порт е онзи, който никога не е бил отварян. Тунелът просто спестява и последния компромис.