Zero Trust для self-hosted AI-сервера: Cloudflare Tunnel, Access и WebAuthn без единого открытого порта

Если вы держите AI-сервер дома или на bare-metal в дата-центре, классическая схема такая: открываете порты 22, 80, 443 на роутере, вешаете fail2ban и надеетесь, что китайские ботнеты вас не найдут. Спойлер: найдут. И попробуют подобрать пароль к SSH быстрее, чем вы допьёте кофе.

Я перевёл свой Bronevik AI-сервер на Zero Trust-архитектуру: ни одного открытого порта наружу, вся публичная поверхность — через Cloudflare Tunnel, аутентификация — через Cloudflare Access с WebAuthn/YubiKey. SSH — только по FIDO2-ключам. В этой статье — пошаговый разбор архитектуры, настройки и пары неочевидных граблей.

Почему «просто закрыть порты» недостаточно

Даже если вы заменили парольную аутентификацию на ключи, открытый 22-й порт — это всё ещё вектор атаки. Ботнеты сканируют весь IPv4 за 45 минут. SSH-демон принимает соединения — значит, есть поверхность для zero-day в OpenSSH. Fail2ban банит по IP, но IP ротируются быстрее, чем вы дописываете правила.

Альтернатива: Cloudflare Tunnel (бывший Argo Tunnel). Это не VPN, это reverse proxy на стероидах. Вместо того чтобы открывать порты наружу, вы запускаете легковесный демон cloudflared внутри сервера. Он инициирует исходящее соединение к Cloudflare, и весь входящий трафик идёт через этот зашифрованный тунель. Ваш внешний IP не нужен вообще.

Плюс Cloudflare Access добавляет слой аутентификации поверх: любой, кто стучится к вашему сервису, сначала попадает на страницу входа Cloudflare с MFA, и только после проверки доходит до вашего приложения.

Архитектура: что куда

Вот как выглядит схема на Bronevik AI-сервере:

Интернет


Cloudflare Edge (любой запрос к hermes.vdsservers.org)

   ├── Cloudflare Access (проверка: кто ты?)
   │      │
   │      ├── Email + OTP (TOTP)
   │      └── WebAuthn (YubiKey)


Cloudflare Tunnel (cloudflared)


Hermes Agent (localhost:9119)
Codex web shell (localhost:9022)
Claude Code web shell (localhost:9023)
Admin-панель (localhost:9090)

Главное: порты 22, 80, 443, 9119, 9022, 9023 — закрыты наружу полностью. Даже если вы просканируете внешний IP сервера, вы не увидите ни одного из этих сервисов. Cloudflare принял запрос, проверил пользователя, и только тогда докинул до локального порта внутри тунеля.

Шаг 1: Cloudflare Tunnel — ставим за 5 минут

Домен у вас уже должен быть на Cloudflare (NS-сервера). Если нет — сначала переведите домен.

# Установка cloudflared на Linux
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared

# Аутентификация (откроет браузер или даст ссылку)
cloudflared tunnel login

# Создаём тунель
cloudflared tunnel create bronevik-tunnel

# Получаем tunnel ID + JSON-файл с credentials в ~/.cloudflared/

Теперь настраиваем маршрутизацию. Файл ~/.cloudflared/config.yml:

tunnel: <YOUR_TUNNEL_ID>
credentials-file: /root/.cloudflared/<YOUR_TUNNEL_ID>.json

ingress:
  - hostname: hermes.vdsservers.org
    service: http://localhost:9119
  - hostname: shell.vdsservers.org
    service: http://localhost:9022
  - hostname: admin.vdsservers.org
    service: http://localhost:9090
  - service: http_status:404

DNS записи создаются автоматически при запуске тунеля (или вручную через cloudflared tunnel route dns). Запускаем:

cloudflared tunnel run bronevik-tunnel

На этом этапе сервисы доступны через Cloudflare, но без аутентификации — любой, кто знает hostname, попадёт на ваш Hermes Agent. Это опасно. Добавляем Access.

Шаг 2: Cloudflare Access — ставим гейт с MFA

Cloudflare Access — это Zero Trust-прокси, который вставляется между пользователем и вашим сервисом. Не доходит до тунеля, пока Access не проверит личность.

Настройка через Cloudflare Zero Trust Dashboard (dash.teams.cloudflare.com):

  1. Applications → Add application → Self-hosted
  2. Указываем hermes.vdsservers.org как домен приложения
  3. В Policies создаём правило: Allow для конкретного email (вашего)
  4. В Settings включаем сессию (например, на 24 часа — чтобы не вводить YubiKey каждый раз)
  5. Сохраняем

Повторяем для shell.vdsservers.org и admin.vdsservers.org.

Теперь при заходе на hermes.vdsservers.org пользователь видит страницу Cloudflare Access: «Введите email». На указанный email приходит одноразовый код (OTP). После ввода кода — доступ.

Шаг 3: WebAuthn/YubiKey — вместо OTP по email

OTP — хорошо, но YubiKey — лучше. WebAuthn (стандарт FIDO2) использует аппаратный ключ: физическое устройство, которое нужно вставить в USB-порт и прикоснуться к нему. Украсть такой фактор удалённо невозможно.

Настройка в Cloudflare Zero Trust:

  1. Settings → Authentication → Login methods
  2. Добавляем Security Key (WebAuthn)
  3. При следующем входе Cloudflare предложит зарегистрировать YubiKey
  4. Тыкаем пальцем в ключ — готово

Теперь процесс входа выглядит так:

  1. Заходим на hermes.vdsservers.org
  2. Вводим email
  3. Вместо OTP — запрос: «Insert your security key»
  4. Вставляем YubiKey, касаемся контакта
  5. Доступ открыт

Второй фактор — то, что у вас физически в кармане, а не код в почте, которую могли перехватить.

Моя грабля: Access + WebAuthn на тестовом поддомене

При настройке Access на тестовом поддомене (test.vdsservers.org) я получил редирект-петлю. Оказалось, Cloudflare Access требует HTTPS на endpoint’е, а мой локальный сервис слушал только HTTP. Решение: в config.yml указать originRequest: noTLSVerify: true для конкретного ingress-правила (только для тестового — в проде так не делайте).

Шаг 4: SSH через YubiKey (FIDO2 ed25519-sk)

Остался SSH. Вариантов два:

Вариант А (простой): SSH через Cloudflare Tunnel + браузерный терминал (Cloudflare Access for SSH). Вы заходите на ssh.vdsservers.org через браузер, проходите Access, и получаете web-терминал. Плюс: не нужно ставить клиент. Минус: латенси и отсутствие привычного workflow.

Вариант Б (мой): FIDO2 SSH-ключи. Генерируем ключ, привязанный к YubiKey:

ssh-keygen -t ed25519-sk -O resident -C "bronevik-yubikey"

Это создаёт ключевую пару, где приватная часть хранится на YubiKey и никогда не покидает устройство. При каждом подключении нужно физическое касание ключа.

# На сервере — добавить публичный ключ в authorized_keys как обычно
cat ~/.ssh/id_ed25519_sk.pub >> ~/.ssh/authorized_keys

Теперь SSH выглядит так:

ssh -i ~/.ssh/id_ed25519_sk user@server
# YubiKey мигает → касаемся → подключение

Никаких паролей. Физический ключ + касание = второй фактор. Потеряли YubiKey? Никто не подключится.

Важно: FIDO2 SSH-ключи (ed25519-sk) работают с OpenSSH 8.2+. На старых системах может не быть поддержки — проверьте ssh -V.

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

Cloudflare Tunnel добавляет минимальную задержку: плюс 1-5 мс через глобальную сеть Cloudflare. Для AI-ворклоуда (Hermes Agent, Codex, Claude Code) это незаметно — HTTP-запросы и так занимают десятки-сотни миллисекунд.

Единственный нюанс — WebSocket-соединения (используются в web-шеллах Codex и Claude Code). Cloudflare поддерживает WebSocket через Tunnel, но максимальный таймаут на бесплатном плане — 100 секунд. Для интерактивной работы с агентом этого более чем достаточно: код-ревью, дебаг, генерация — редко укладываются в сессию длиннее пары минут.

Что делать, если Cloudflare лёг

Самый частый вопрос: «А если Cloudflare упадёт? Как я попаду на сервер?»

Ответ: fallback-доступ. У вас должен быть способ попасть на сервер, не зависящий от Cloudflare:

  • Прямой IPMI/iDRAC/iLO — если сервер в дата-центре, у вас есть KVM-доступ
  • Резервный WireGuard VPN на отдельном порту (например, 51820) — только для экстренного доступа, с тем же FIDO2-ключом
  • SSH через Tailscale — работает поверх WireGuard, не требует открытых портов, использует NAT traversal

У меня: основной доступ через Cloudflare Tunnel + Access (это 99% времени), резервный — WireGuard VPN до сервера с тем же YubiKey. Cloudflare падал за 3 года дважды, оба раза на 20-30 минут. За это время я просто подождал — ничего критичного.

Сравнение с альтернативами

РешениеОткрытые портыMFAБесплатноСложность
Прямой SSH (port 22)22 ❌Нет ❌Да ✅Низкая
WireGuard VPN51820 ❌Нет ❌Да ✅Средняя
TailscaleНет ✅Через IdP ✅До 3 users ✅Низкая
Cloudflare Tunnel + AccessНет ✅WebAuthn/YubiKey ✅До 50 users ✅Низкая
Cloudflare Tunnel + Access + FIDO2 SSHНет ✅Аппаратный ключ ✅Да ✅Средняя

Tailscale — отличная альтернатива, если вам нужен mesh-VPN между устройствами. Но если задача — выставить наружу конкретный web-сервис (Hermes Agent, админку, web-шелл) с аутентификацией, не открывая портов — Cloudflare Tunnel + Access выигрывает по соотношению простота/безопасность.

Реальный опыт: что пошло не так

За год использования этой схемы было три инцидента:

  1. Access-политика заблокировала меня же. После изменения email в настройках Cloudflare я 40 минут не мог зайти на сервер — политика была привязана к старому email. Решение: менять email через Cloudflare Dashboard (доступен без Access), а не через настройки приложения.

  2. YubiKey отказал после обновления прошивки. Ключ перестал определяться в Linux. Решение: иметь второй зарегистрированный YubiKey в Access-политике (и в authorized_keys). Сейчас у меня два ключа: основной на связке, резервный в сейфе.

  3. Cloudflare Access редирект-петля. При первом запуске браузер бесконечно редиректил между Cloudflare и локальным сервисом. Проблема была в том, что Access ожидал HTTPS, а локальный nginx слушал HTTP. Решение: в config.yml добавить originRequest: httpHostHeader, либо настроить самоподписанный сертификат на локальном endpoint’е.

Итог: Zero Trust за 35 минут

Полная настройка заняла у меня 35 минут с нуля:

  • 5 минут — установка cloudflared и создание тунеля
  • 10 минут — настройка DNS и ingress-правил
  • 10 минут — Cloudflare Access с WebAuthn/YubiKey
  • 5 минут — генерация ed25519-sk ключа для SSH
  • 5 минут — проверка всего

Результат: ни одного открытого порта на внешнем IP, полная изоляция self-hosted AI-инфраструктуры, аутентификация через аппаратный ключ. Спать стало спокойнее.

Если вы запускаете AI-сервер дома или на своём bare-metal — это минимальный уровень безопасности, который стоит настроить до того, как вы начнёте экспериментировать с агентами.