Knowledge Base

Cloudflare Access — Zero Trust защита для админ-панели

Cloudflare Access — Zero Trust для /nr-control/

Категория: Server Hardening · Риск: info

Cloudflare Access — Zero Trust для /nr-control/

Что такое Cloudflare Access

Cloudflare Access — бесплатный (до 50 пользователей) Zero Trust шлюз. Он **перехватывает запросы до nginx** и требует аутентификации через email OTP, GitHub, Google, Azure AD или любой OIDC-провайдер.

Для пользователя это выглядит как дополнительная страница входа перед тем, как попасть на `/nr-control/`. Для сервера — Cloudflare добавляет подписанный JWT в заголовок `Cf-Access-Jwt-Assertion`, который nginx/backend могут проверить.

Почему это лучше просто IP allowlist

| Метод | Смена IP | Мобильный | Команда | MFA | |-------|----------|-----------|---------|-----| | IP allowlist | ломается | сложно | нужно добавлять каждого | нет | | Cloudflare Access | не зависит | работает | email-приглашения | да | | Tailscale | не зависит | работает | удобно | да |

Настройка Cloudflare Access (15 минут)

Шаг 1 — Zero Trust Dashboard

cloudflare.com → Zero Trust → Access → Applications
→ Add an Application → Self-hosted

Шаг 2 — Параметры приложения

Application name:   NodeRoute Admin
Session Duration:   24 hours
Application domain: noderoute.ru
Path:               nr-control/
                    (также добавьте: api/nr-admin/)

Шаг 3 — Policy (политика доступа)

Policy name: Admins
Action: Allow
Include:
  → Emails ending in: @yourcompany.com
  или
  → Emails: admin@gmail.com, teammate@gmail.com

Require (для MFA):
  → Authentication method: mTLS / One-time PIN

Шаг 4 — Login method

Zero Trust → Settings → Authentication
→ Add: One-time PIN (бесплатно, нет нужды в провайдере)
→ Или: GitHub / Google OAuth

Проверка JWT на стороне nginx (опционально, рекомендуется)

После включения Access Cloudflare добавляет заголовок. Nginx может его проверить:

# Проверка наличия CF Access JWT
location /nr-control/ {
    # Cloudflare Access автоматически блокирует доступ без JWT
    # Дополнительно можно проверить заголовок на backend-уровне:

    # Разрешить только Cloudflare IP (они добавляют JWT)
    include /etc/nginx/cloudflare-ips.conf;
    deny all;

    alias /usr/share/nginx/html/admin/;
    try_files $uri $uri/ /admin/index.html;
    ...
}
# Скачать актуальный список IP Cloudflare
curl -s https://www.cloudflare.com/ips-v4 | \
    awk '{print "allow "$1";"}' > /etc/nginx/cloudflare-ips.conf
curl -s https://www.cloudflare.com/ips-v6 | \
    awk '{print "allow "$1";"}' >> /etc/nginx/cloudflare-ips.conf

Проверка JWT в FastAPI (максимальная защита)

# app/core/admin_auth.py — дополнительная проверка CF Access JWT
import jwt  # pip install PyJWT
import httpx

_CF_CERTS_URL = "https://<your-team>.cloudflareaccess.com/cdn-cgi/access/certs"
_CF_AUD = "<application-audience-tag>"  # из настроек Access

async def verify_cf_access_jwt(request: Request) -> None:
    """Verify Cloudflare Access JWT. Call from require_admin if CF Access is enabled."""
    token = request.headers.get("Cf-Access-Jwt-Assertion")
    if not token:
        raise HTTPException(status_code=401, detail="CF Access token missing")

    async with httpx.AsyncClient() as client:
        certs = (await client.get(_CF_CERTS_URL)).json()

    public_keys = [jwt.algorithms.RSAAlgorithm.from_jwk(k) for k in certs["keys"]]
    for key in public_keys:
        try:
            payload = jwt.decode(token, key, algorithms=["RS256"], audience=_CF_AUD)
            return  # valid
        except jwt.InvalidTokenError:
            continue
    raise HTTPException(status_code=401, detail="Invalid CF Access token")

Мониторинг попыток доступа

В Cloudflare Zero Trust → Logs → Access вы видите:

Когда использовать что

| Ситуация | Рекомендация | |----------|-------------| | Один администратор, фиксированный IP | `ADMIN_ALLOWED_IPS` в .env | | Команда или меняющийся IP | Cloudflare Access | | Максимальная безопасность | Tailscale + отдельный порт | | Уже есть WireGuard/Tailscale | Туннель через VPN |

Связанные проверки