24-cloudflare — отказоустойчивый app-ingress через Cloudflare (mitdev cf)
☐ Опциональный, командный. В отличие от обычных модулей его не «ставят» одной галочкой в установщике — это набор подкоманд
mitdev cf …, которыми узел вводится в отказоустойчивый ingress через Cloudflare: туннель (без входящих портов), DNS и Load Balancing — всё только через API Cloudflare, плюс два локальных демона (health-эндпоинт и tunnel-watchdog) для честного failover на уровне приложения.
Зачем нужен
Отказоустойчивость баз данных (HA.md) отвечает на вопрос «переживёт ли смерть узла состояние». Этот модуль отвечает на другой: как пользователи попадут на живой узел приложения, когда один из узлов лёг или начал обслуживать ошибки — и как убрать больной узел из-под трафика, не открывая при этом ни одного входящего порта наружу.
Идею проще всего понять через три проблемы, которые она закрывает:
- Не нужно выставлять узлы в интернет. cloudflared держит исходящее соединение к сети Cloudflare (edge) и принимает трафик оттуда. На узле можно закрыть входящие 80/443 полностью — снаружи не видно ни одного открытого порта, а сайт при этом работает.
- «Серый отказ» становится виден. Узел, у которого жив веб-сервер, но сломан путь к БД или Redis, отдаёт пользователю ошибки, оставаясь «живым» для простого пинга. Локальный health-эндпоинт отвечает
200только если живы app И БД И Redis — иначе503. По этому сигналу узел выводится из ротации. - Failover без ручного вмешательства. Локальный tunnel-watchdog при устойчивом
503останавливает cloudflared → узел исчезает из ротации Cloudflare за секунды. Cloudflare Load Balancer дублирует это со стороны edge (health-монитор). Восстановился health — узел возвращается.
Термины, которые встретятся ниже:
- cloudflared — демон-коннектор Cloudflare Tunnel. Устанавливает исходящее соединение к edge (QUIC на udp/7844, фолбэк http2 на tcp/7844) и проксирует через него входящий трафик на локальные сервисы. Входящих портов на узле не требует.
- remotely-managed туннель (
config_src: cloudflare) — режим, при котором ingress-правила туннеля (какой домен на какой локальный адрес) хранятся в Cloudflare и правятся через API. Локальногоconfig.ymlна узле нет. - connector-token — секрет, которым конкретный cloudflared авторизуется как коннектор своего туннеля. По ценности равен паролю: кто им владеет, тот принимает трафик туннеля.
- origin — «источник» в терминах Load Balancing: конечная точка, куда LB шлёт трафик. У нас origin — либо туннель узла (
<uuid>.cfargotunnel.com), либо публичный IP узла. - pool (пул) — группа origin'ов с привязанным health-монитором. LB раскидывает трафик по здоровым origin'ам пула.
- health-монитор — проба Cloudflare, которая периодически стучит в origin и решает, здоров ли он. У нас проба уводится в health-эндпоинт узла (см. «Скрытый health-хост»).
- proxied («оранжевое облако») — DNS-запись, трафик которой идёт через Cloudflare (а не напрямую на указанный адрес). CNAME на
cfargotunnel.comработает только проксированным.
Картина целиком (режим по умолчанию lb-over-tunnel)
пользователи
│
app.example.com
│
┌───────────▼────────────┐
│ Cloudflare (edge) │
│ Load Balancer │
│ └ pool │
│ ├ origin: туннель-1 │ health-монитор →
│ ├ origin: туннель-2 │ Host: health.app.example.com
│ └ origin: туннель-3 │ ждёт 200
└───────┬──────┬──────┬────┘
QUIC/7844 │ │ │ (исходящее от узлов; входящих портов нет)
┌─────────────┘ │ └─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ узел-1 │ │ узел-2 │ │ узел-3 │
│cloudflared│ │cloudflared│ │cloudflared│
│ nginx→app│ │ nginx→app│ │ nginx→app│
│ health :8009 200/503 (loopback/WG, не наружу) │
│ tunnel-watchdog: 503×N → stop cloudflared │
└──────────┘ └──────────┘ └──────────┘
Каждый app-узел держит свой туннель; каждый туннель — origin в одном LB-пуле. Здоровье узла оценивается дважды: локально (tunnel-watchdog, снимает узел за ~15 с) и со стороны edge (health-монитор LB, второй эшелон, ~3 мин). Данные не фрагментируются — все узлы работают с общей БД (см. HA.md).
Три режима ingress (CF_INGRESS_MODE)
| Режим | Транспорт | origin в пуле | Когда |
|---|---|---|---|
lb-over-tunnel (дефолт) |
туннель на каждый узел | <uuid>.cfargotunnel.com |
Полный HA: нет входящих портов + health/гео/affinity от LB |
tunnel-only |
туннель на каждый узел | — (LB не используется) | Один узел или round-robin по DNS; LB не нужен |
lb-only |
публичные IP узлов | публичный IP узла | LB поверх узлов без туннелей |
Команды согласованы с режимом: cf lb … в tunnel-only честно откажет (LB там не нужен), а origin-адреса в пуле различаются по режиму.
⚠️
lb-onlyи health-failover. Вlb-onlyтуннеля нет, а health-эндпоинт слушает loopback/WG и из сети Cloudflare недостижим — health-монитору некому увести пробу в него. Поэтомуcf lb monitor/pool/createв режимеlb-onlyнамеренно отказывают fail-loud (health-failover в этом режиме не обеспечить). Для полного HA используйтеlb-over-tunnel.
Четыре варианта применения: tunnel и LB в разных сочетаниях
Три режима CF_INGRESS_MODE выше — это технический enum. На практике из них складываются четыре варианта применения: tunnel-only распадается на «один узел» и «несколько узлов через round-robin DNS», а «чистый LB без туннеля» существует лишь как отклоняемый режим lb-only. Матрица выбора:
| Вариант | Туннель | LB | Узлов | Health-failover | Входящие порты | Поддержка в mitdev |
|---|---|---|---|---|---|---|
1. lb-over-tunnel (рекоменд.) |
✅ на каждом узле | ✅ поверх туннелей | 1..N | ✅ два эшелона (watchdog + LB-монитор) | закрыты | полная |
2. tunnel-only, один узел |
✅ один | ❌ | 1 | — (резерва нет) | закрыты | полная |
3. tunnel-only + round-robin DNS |
✅ на каждом узле | ❌ (DNS-раскидка) | 2..N | ❌ мёртвый узел держит свою долю трафика | закрыты | частичная (LB нет; watchdog грубо смягчает) |
4. LB по публичным IP без туннеля (lb-only) |
❌ | ✅ по публичным IP | 1..N | ❌ health-эндпоинт из edge недостижим | открыты 80/443 | ❌ cf lb … падает fail-loud |
Вариант 1 — lb-over-tunnel (несколько узлов, полный HA) — по умолчанию
Туннель на каждом узле + Cloudflare Load Balancer поверх туннелей. Здоровье узла проверяется дважды: локальный tunnel-watchdog (15 с) и health-монитор LB со стороны edge (3 мин). Входящих портов на узлах нет. Это единственный вариант с настоящим health-failover между узлами. Полное развёртывание — в разделе «Развёртывание» ниже.
Работает и на одном узле (пул из одного origin) — но если узел один и рост не планируется, проще Вариант 2.
Вариант 2 — tunnel-only, один узел
Один туннель, одна DNS-запись на него, LB не нужен. Балансировать нечего, поэтому cf lb … в этом режиме честно откажет.
export CF_INGRESS_MODE=tunnel-only
sudo -E mitdev cf login
sudo mitdev cf health install # опционально
sudo mitdev cf watchdog install # опционально
sudo mitdev cf tunnel create app
sudo mitdev cf tunnel config app app.example.com
sudo mitdev cf tunnel run # cloudflared + закрыть вход 80/443
sudo mitdev cf dns set app.example.com CNAME <tunnel-id>.cfargotunnel.com
cf dns set здесь обязателен (в lb-over-tunnel DNS-запись создаёт cf lb create). health/watchdog опциональны: при локальном отказе watchdog остановит cloudflared, и узел станет недоступен — резерва, куда увести трафик, в этом варианте нет.
Вариант 3 — tunnel-only + round-robin DNS (несколько узлов, без health-failover)
По туннелю на узел (как в Варианте 2), плюс несколько CNAME-записей на один хост — Cloudflare отдаёт их по очереди:
# на каждом узле — свой туннель в режиме tunnel-only
sudo mitdev cf dns set app.example.com CNAME <uuid-узел-1>.cfargotunnel.com
sudo mitdev cf dns set app.example.com CNAME <uuid-узел-2>.cfargotunnel.com
sudo mitdev cf dns set app.example.com CNAME <uuid-узел-3>.cfargotunnel.com
⚠️ Нет health-проверки узлов. Round-robin DNS не знает о здоровье origin'ов: при падении одного из трёх узлов ~1/3 запросов продолжит лететь на мёртвый узел, пока запись не убрать вручную (
cf dns rm). Нет второго эшелона — того самого скрытого health-хоста, ради которого существует LB.cf watchdog installчастично смягчает (остановит cloudflared → Cloudflare вернёт ошибку коннектора для этого туннеля), но это грубее health-failover и не ловит «серый отказ», когда туннель жив, а app отдаёт503. Нужен настоящий межузловой failover — берите Вариант 1.
Вариант 4 — LB по публичным IP без туннеля (lb-only) — в mitdev не поддержан
«Чистый LB без туннеля» соответствует режиму lb-only, но команды cf lb monitor/pool/create в нём намеренно завершаются с ошибкой (_cf_lb_reject_lb_only). Причина структурная: origin пула — публичный IP узла, cloudflared на узле нет, а health-эндпоинт слушает loopback/WG и из сети Cloudflare недостижим. Увести пробу монитора (Host: health.<домен>) в скрытый health-хост без туннеля некому — она упрётся в публичный веб-сервер узла. Итог был бы один из двух плохих:
- проба не доходит → все origin'ы
unhealthy→ пустой пул → полный отказ ingress; либо - монитор меряет лишь живость веб-сервера → «серый отказ» (app жив, путь к БД мёртв) остаётся невидимым.
Чтобы не создавать ложное чувство HA, mitdev такую конструкцию не собирает. Режим lb-only остаётся в enum, но команды cf lb в нём падают fail-loud с подсказкой переключиться на lb-over-tunnel. Если туннель принципиально не нужен (узлы обязаны принимать трафик напрямую по публичным IP), Cloudflare Load Balancing настраивается вручную в аккаунте с публичным health-эндпоинтом — health-эндпоинт mitdev для этого не подходит (он не рассчитан торчать наружу).
Как выбрать вкратце: несколько узлов и нужен failover → Вариант 1; один узел → Вариант 2; несколько узлов, но простой части трафика на минуты допустим → Вариант 3; туннель недопустим по требованиям → LB руками вне mitdev (Вариант 4).
Скрытый health-хост (ключевое решение)
Health-эндпоинт по требованию безопасности слушает loopback или WG-адрес, никогда 0.0.0.0 — иначе карта здоровья узлов (коды 200/503 и причина отказа в теле) светила бы наружу и стала бы дешёвой точкой разведки и DoS на пути failover. Но тогда health-монитор Cloudflare, живущий в сети edge, не может достучаться до эндпоинта напрямую.
Решение: скрытый health-хост. В ingress-правила туннеля добавляется правило health.<домен> → http://localhost:CF_HEALTH_PORT перед правилом приложения. Health-монитор бьёт в origin-туннель, подставляя заголовок Host: health.<домен> — cloudflared по этому Host уводит пробу в health-эндпоинт, минуя приложение на 80-м порту. Публичной DNS-записи на health.<домен> не создаётся и быть не должно: снаружи он не резолвится, карта здоровья наружу не видна.
Имя health-хоста собирается ровно в одном месте кода (одна функция) — и для правила туннеля, и для заголовка монитора: разъехались бы они, проба ушла бы в приложение, и серый отказ снова стал бы невидимым.
Безопасность API-токена
Взаимодействие с Cloudflare идёт через curl к api.cloudflare.com/client/v4. Токен — самый чувствительный секрет модуля, и с ним обращаются строго:
- Только в кредах.
cf loginсохраняетCF_API_TOKENв файл учётных данных с правами600. В коде токен читается оттуда, а не передаётся в открытую. - Никогда в
ps/argv. И заголовокAuthorization: Bearer …, и тело запроса (в котором у создания туннеля едетtunnel_secret) уходят curl одним конфигом на stdin (curl --config -), а не аргументами командной строки — argv процесса виден любому локальному пользователю черезps//proc/<pid>/cmdline. - Connector-token — через
EnvironmentFile. systemd-юнит cloudflared читаетTUNNEL_TOKENиз/etc/mitdev/cf-tunnel.env(права600), аExecStartне содержит токена. Написатьrun --token ${TUNNEL_TOKEN}в юните нельзя: systemd раскрывает переменную в момент exec, и токен утёк бы вps. - Редакция в логах.
redact_log_textвырезаетAuthorization: Bearer …иCF_API_TOKEN=…. Но защита держится в первую очередь на том, что секрет в лог не кладётся по построению: логируются только метод, путь и HTTP-код. - Минимальные права токена. Нужны ровно: Account → Cloudflare Tunnel: Edit; Zone → DNS: Edit; Zone → Load Balancers: Edit; Account → Load Balancing: Monitors and Pools: Edit; Account → Account Settings: Read. Меньше прав — меньше ущерб при утечке.
Fail-loud и идемпотентность
- Fail-loud. Каждый ответ API проверяется на
success:true; приerrorsпечатается человекочитаемая причина со списком нужных scope, без тихого продолжения. Обёрткаcf_apiразличает исходы:2xx+success(ок), отказ по делу (success:false/4xx), сбой сети/curl, и деградация (5xx/429 — Cloudflare ответил, но лежит или ограничил темп). Последнее важно: при деградации оператору не говорят «токен невалиден» — менять исправный токен, пока лежит сам Cloudflare, бесполезно. - Идемпотентность. Все создающие команды сперва ищут ресурс по имени (у DNS — по имени и типу; у мониторов — по
description, так как поляnameу них нет) и делаютupdate(PUT) вместо повторногоcreate(POST). Повторный запуск любой команды не плодит дублей туннелей, записей, мониторов, пулов и балансировщиков. Имена нормализуются к нижнему регистру (Cloudflare так делает на своей стороне), DNS-имена требуют FQDN — иначе повторный запуск создал бы дубль записи в зоне.
Локальные демоны
health-эндпоинт узла
Персистентный socat-листенер (не socket-activation — чтобы частый опрос не упирался в start-limit systemd) на CF_HEALTH_PORT (8009), bind на CF_HEALTH_BIND_IP (loopback или WG-адрес). На каждый коннект проверяет локально:
- app — TCP-коннект на
CF_APP_PORT(порт локального фронта, дефолт 80); - БД —
pg_isreadyк pg-proxy (127.0.0.1:5432); - Redis —
PINGк локальному redis-proxy.
Отвечает 200, только если все три живы; иначе 503 с указанием отказавшего компонента. Логика «любой отказ → 503» намеренна: узел с живым app, но мёртвым путём к БД обслуживает ошибки — для ротации он обязан выглядеть больным. Юнит бежит под User=nobody с NoNewPrivileges.
Проверки fail-closed: если
pg_isready/redis-cliна узле не установлены, health отвечает503. Поэтомуcf health installставитpostgresql-clientиredis-tools(на RHEL —postgresql/redis) наряду сsocat.
tunnel-watchdog
Демон, который каждые CF_TUNNEL_CHECK_INTERVAL (5 с) бьёт curl в health-эндпоинт с таймаутом CF_TUNNEL_CURL_TIMEOUT (3 с) и по устойчивому сигналу управляет cloudflared:
503подрядCF_TUNNEL_FAIL_THRESHOLDраз (3) →systemctl stop mitdev-cf-tunnel→ узел исчезает из ротации Cloudflare (~15 с).200подрядCF_TUNNEL_RESTORE_THRESHOLDраз (2) →systemctl start→ узел возвращается.
Дебаунс (пороги, а не первый же промах) — против флаппинга: моргание mesh или единичный 503 не должны дёргать stop/start. Порог возврата ниже порога снятия (2 < 3): доступность важна, но не с первого же 200.
Различает 503 и «эндпоинт недоступен». 503 — это диагноз app-уровня (снимать после порога). А если сам эндпоинт не отвечает (не путать!) — это отдельный сигнал, он логируется, но по нему узел не снимается по ложной тревоге. И главное — watchdog снимает узел только по node-local отказу (мёртв локальный app). По общей зависимости (лежит общая БД/Redis) узел не снимается: иначе падение общего компонента вывело бы из ротации все узлы разом (анти-паттерн каскадного отказа).
Firewall: закрыть вход, не отрезав туннель
cf tunnel run закрывает входящие 80/443, но делает это в безопасном порядке:
- Сначала разрешает исходящие
443/tcp,7844/tcpи7844/udp(QUIC к edge + http2-фолбэк) — до закрытия входа. При дефолтной политикеallow outgoingэто no-op, но если оператор ужесточил исходящие, без этих правил туннель молча не поднимется. - Ставит cloudflared, поднимает юнит и убеждается, что коннектор активен (
systemctl is-active). - Только теперь закрывает вход 80/443 — и подтверждает фактическое состояние по
ufw status(не «удалили правило», а «правила больше нет»). Обратный порядок при неудачном старте туннеля оставил бы узел без веб-входа И без туннеля — полный отказ ingress. - Откат каждого правила регистрируется до его удаления; SSH не трогается ни при каких условиях.
После закрытия 80/tcp перестаёт работать ACME HTTP-01 (модуль 08-certbot) — при ingress через туннель TLS терминирует Cloudflare, обычный сертификат узлу не нужен; об этом печатается предупреждение.
Подкоманды
Полные примеры — в ../COMMANDS.md. Кратко:
| Подкоманда | Что делает |
|---|---|
cf login |
Проверить CF_API_TOKEN (/user/tokens/verify), сохранить token/account/zone в креды (600) |
cf health install / remove |
Поднять/снять health-эндпоинт узла |
cf watchdog install / remove |
Поднять/снять tunnel-watchdog |
cf tunnel create <имя> |
Создать (или переиспользовать) remotely-managed туннель; id и connector-token → в креды |
cf tunnel config <имя> <домен> [url] |
Записать ingress-правила туннеля в Cloudflare (health-хост + домен→app + catch-all 404) |
cf tunnel run |
Поставить cloudflared, поднять юнит, закрыть вход 80/443 |
cf tunnel remove |
Снять коннектор и вернуть вход 80/443 |
cf dns set <имя> <тип> <значение> [proxied] |
Идемпотентно создать/обновить запись зоны (A/AAAA/CNAME/TXT) |
cf dns rm <имя> <тип> |
Удалить запись (идемпотентно) |
cf lb monitor <домен> |
Создать/обновить health-монитор (проба в скрытый health-хост) |
cf lb pool <имя> <origin…> |
Создать/обновить пул origin'ов с монитором |
cf lb create <домен> |
Создать/обновить балансировщик на зоне поверх пула |
cf status |
Сводка: токен, туннели и их коннекторы, пулы и здоровье origin'ов, локально cloudflared/health/watchdog |
cf status не врёт
status — единственная команда модуля, которой нельзя умирать на первом отказе API: оператор смотрит её именно во время инцидента, когда часть API/узлов уже лежит. Поэтому каждая секция ловит свой отказ сама, продолжает опрашивать остальные, и никогда не печатает «здорово» по секции, которую не смогла опросить («не разобрано» ≠ «здорово»). Код возврата 0 — только если всё опрошено и всё здорово; любой отказ опроса или нездоровый компонент → rc ≠ 0 (годится как гейт в мониторинге). Причины различены честно: «сеть», «Cloudflare деградировал (HTTP 5xx/429)», «токен невалиден», «креды недоступны без root» — это разные диагнозы, требующие разных действий.
Настройка
| Переменная | По умолчанию | Влияние | Когда менять |
|---|---|---|---|
CF_API_TOKEN |
пусто | scoped API-токен Cloudflare | задаётся перед cf login |
CF_ACCOUNT_ID |
пусто | id аккаунта (нужен для tunnel/lb) | перед tunnel/lb |
CF_ZONE_ID |
пусто | id зоны (нужен для dns/lb) | перед dns/lb |
CF_INGRESS_MODE |
lb-over-tunnel |
режим ingress (см. таблицу режимов) | tunnel-only/lb-only под свою топологию |
CF_HEALTH_PORT |
8009 |
порт health-эндпоинта | при конфликте порта |
CF_HEALTH_BIND_IP |
127.0.0.1 |
адрес bind health-эндпоинта (loopback/WG) | WG-адрес, если монитор/сосед бьёт по mesh; никогда 0.0.0.0 |
CF_APP_PORT |
80 |
порт локального фронта, который проверяет health | если фронт не на 80 |
CF_HEALTH_HOST_PREFIX |
health |
префикс скрытого health-хоста <prefix>.<домен> |
при конфликте с реальным поддоменом |
CF_TUNNEL_UNIT |
mitdev-cf-tunnel |
имя systemd-юнита cloudflared | если cloudflared поставлен под именем cloudflared |
CF_TUNNEL_FAIL_THRESHOLD |
3 |
сколько 503 подряд до снятия узла |
реже/чаще снимать |
CF_TUNNEL_RESTORE_THRESHOLD |
2 |
сколько 200 подряд до возврата |
симметрия возврата |
CF_TUNNEL_CHECK_INTERVAL |
5 |
интервал опроса health, с | плотность опроса |
CF_TUNNEL_CURL_TIMEOUT |
3 |
таймаут одной пробы, с | < интервала |
CF_LB_MONITOR_INTERVAL |
60 |
период пробы LB-монитора, с | второй эшелон, учащать незачем |
CF_LB_MONITOR_TIMEOUT |
5 |
таймаут пробы монитора, с | < интервала |
CF_LB_MONITOR_RETRIES |
2 |
провалов подряд до «origin мёртв» | анти-флаппинг |
Развёртывание (режим по умолчанию, три узла на общей БД)
Предполагается: приватная сеть (MESH.md), общая БД доступна с каждого узла (кластер + pg proxy, см. HA.md), домен app.example.com в зоне Cloudflare. Токен в чат/на чужой сервер не передавайте — экспортируйте локально перед командами.
На каждом app-узле:
export CF_API_TOKEN=… CF_ACCOUNT_ID=… CF_ZONE_ID=…
sudo -E mitdev cf login # проверить токен, сохранить в креды
sudo mitdev cf health install # health-эндпоинт (200 если app+БД+Redis живы)
sudo mitdev cf watchdog install # tunnel-watchdog (снимает узел при 503)
sudo mitdev cf tunnel create узел-1 # свой туннель на узел
sudo mitdev cf tunnel config узел-1 app.example.com # ingress-правила в CF
sudo mitdev cf tunnel run # cloudflared + закрыть вход 80/443
Один раз (с любого узла), собрать LB поверх туннелей:
# UUID туннелей узлов взять из вывода `cf tunnel create` или `cf status`
sudo mitdev cf lb monitor app.example.com
sudo mitdev cf lb pool app-pool <uuid-узел-1> <uuid-узел-2> <uuid-узел-3>
sudo mitdev cf lb create app.example.com
Проверка:
sudo mitdev cf status # токен, туннели/коннекторы, пул/origin'ы, локальные демоны
Проверка, что всё работает
sudo mitdev cf status # сводная картина
systemctl is-active mitdev-cf-tunnel # cloudflared активен
systemctl is-active mitdev-cf-health mitdev-cf-tunnel-watchdog
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8009/ # 200 или 503
Снаружи (со стороны, не с узла): nmap показывает 80/443 закрытыми, а сайт при этом открывается — трафик идёт только через туннель.
⚠️ Перед продакшеном: чекпойнт живого Cloudflare
Весь модуль покрыт офлайн-тестами (API-вызовы — через подменяемую обёртку cf_api, локальные проверки — через стабы). Это проверяет логику, но не проверяет поведение настоящего Cloudflare. Несколько вещей закрываются только на живом аккаунте — прогоните их на стенде до боевого включения:
- Поддерживает ли health-монитор переопределение
Hostдля origin'ов вида<uuid>.cfargotunnel.comи доходит ли проба через туннель до health-эндпоинта (от этого зависит вся схема скрытого health-хоста). - Подхватывает ли ваша версия cloudflared
TUNNEL_TOKENиз окружения. - Реальная форма ответа
/pools/{id}/health(её разбираетcf status). - Поднимается ли
socat TCP-LISTEN:<port>,bind=<ipv6>безpf=ip6, если health биндится на IPv6 WG-адрес.
Частые ошибки
Симптом: cf login говорит «токен невалиден», хотя токен рабочий.
Причина: Cloudflare ответил 5xx/429 (деградация/лимит), либо сетевой сбой.
Решение: актуальный cf login/cf status различают эти случаи и в них токен не обвиняют. Если всё же «невалиден» — проверьте scope токена (список выше) и что он не истёк.
Симптом: после cf lb create сайт не открывается, все origin'ы «unhealthy».
Причина: health-монитор не доходит до health-эндпоинта (проба уходит в приложение) — вероятно, вопрос живого CF про Host-override (см. чекпойнт), либо режим lb-only.
Решение: проверьте cf status; для lb-only вспомните ограничение (health-failover там не обеспечить — используйте lb-over-tunnel).
Симптом: узел не выходит из ротации при сломанной БД.
Причина: tunnel-watchdog не установлен, либо health отдаёт 200 (не проверяет реальный путь к БД).
Решение: systemctl is-active mitdev-cf-tunnel-watchdog; curl 127.0.0.1:8009 — должен быть 503 при сломанной БД; проверьте CF_APP_PORT/порты pg-proxy/redis-proxy.
Симптом: health всегда 503, хотя app/БД/Redis живы.
Причина: на узле нет pg_isready/redis-cli (fail-closed), либо health бьёт не в тот Redis (не proxy-порт), либо IPv6-bind без скобок.
Решение: переустановите cf health install (ставит клиентов); проверьте REDIS_PROXY_PORT; для IPv6-bind — вопрос socat из чекпойнта.
Симптом: после cf tunnel run пропал ACME-сертификат / не продлевается.
Причина: закрыт вход 80/tcp — ACME HTTP-01 больше не пройдёт.
Решение: это ожидаемо: при ingress через туннель TLS терминирует Cloudflare. Если сертификат на узле всё же нужен — переведите продление на DNS-01.
Безопасность и продакшен
- Токен — минимальных прав, только в кредах (600), никогда в
ps/лог. При подозрении на утечку — отозвать в Cloudflare и создать новый (cf login). - health-эндпоинт — никогда
0.0.0.0. Это сигнал failover; публично он выдал бы карту здоровья узлов и причины отказов. - Firewall —
cf tunnel runзакрывает вход только после подтверждения, что коннектор активен, и с откатом; SSH не трогается. Проверьте снаружи, что 80/443 закрыты. - Дебаунс watchdog и монитора — против флаппинга; не занижайте пороги без нужды.
- Живой Cloudflare — прогоните чекпойнт выше на стенде до продакшена. Схема проверена офлайн, но настоящий edge может повести себя иначе в деталях (
Host-override, форма health-ответа).
Откат и удаление
Модуль командный — «удаляется» снятием того, что ставилось:
sudo mitdev cf tunnel remove # снять коннектор, ВЕРНУТЬ вход 80/443
sudo mitdev cf watchdog remove # снять tunnel-watchdog
sudo mitdev cf health remove # снять health-эндпоинт
Ресурсы в самом Cloudflare (туннель, DNS-записи, пул, монитор, балансировщик) удаляются через API/дашборд отдельно — mitdev их не сносит автоматически, чтобы случайный remove на одном узле не обрушил ingress остальных. Установка каждого локального демона регистрирует откат: при сбое установки юнит и файлы снимаются автоматически.
См. также
- ../COMMANDS.md — полные примеры команд
mitdev cf - ../CONFIGURATION.md — переменные
CF_* - ../HA.md — отказоустойчивость БД/Redis, единая точка входа, учения
- ../MESH.md — приватная сеть между узлами (health по WG-адресу)
- ../SECURITY.md — модель секретов и сетевая модель
- 08-certbot.md — TLS и ACME (что меняется при ingress через туннель)
- ../TROUBLESHOOTING.md — диагностика служб