Authentication: basic, session, kerberos, custom backend
Без правильной аутентификации REST API становится либо дырой в безопасности (anonymous access), либо bottleneck-ом (heavy DB lookups на каждый request). В Airflow 2.x за auth отвечает Flask-AppBuilder (FAB) и набор pluggable auth backends, который вы конфигурируете в airflow.cfg.
Этот урок — детальный разбор каждого backend, его внутренней механики, performance implications и правильного выбора под use case.
Архитектура auth в Airflow 2.x
Конфигурация — список backends, которые пробуются по очереди:
[api]
auth_backends = airflow.api.auth.backend.basic_auth,airflow.api.auth.backend.session
Первый backend, который успешно определит user, выигрывает. Если все backends провалились — возвращается 401.
Backend 1: basic_auth
HTTP Basic Authentication — RFC 7617. Самый простой и широко поддерживаемый способ.
GET /api/v1/dags HTTP/1.1
Authorization: Basic YWlyZmxvdzphaXJmbG93
Где YWlyZmxvdzphaXJmbG93 это base64 от airflow:airflow.
Что происходит внутри
# Псевдокод airflow/api/auth/backend/basic_auth.py
def auth_current_user() -> User | None:
auth = request.authorization
if auth is None or not auth.username:
return None
# DB lookup на каждый request!
user = appbuilder.sm.auth_user_db(auth.username, auth.password)
if user is None:
return None
g.user = user
return user
Критично: каждый запрос делает DB lookup + bcrypt verify пароля. Bcrypt cost = 12 по default → 80-120ms per request CPU на webserver. Для high-volume integration это bottleneck.
Сценарии
| Использовать basic_auth когда | Не использовать когда |
|---|---|
| Manual curl/Postman debugging | High-volume polling (>10 RPS на один user) |
| Machine-to-machine с redke calls | Browser-based UI (use session) |
| CI/CD pipeline (десятки calls per build) | Public-facing integration |
Production hardening
[api]
auth_backends = airflow.api.auth.backend.basic_auth
auth_rate_limited = True
auth_rate_limit = 5 per 40 seconds
auth_rate_limited (2.5+) ограничивает попытки неудачной auth — защита от brute-force.
Backend 2: session
Cookie-based session. Используется когда UI делает AJAX запросы к REST API: пользователь уже авторизован через /login, cookie выдан, API использует ту же session.
GET /api/v1/dags HTTP/1.1
Cookie: session=eyJfcGVybWFuZW50Ijp0cnVlLCJfdXNlcl9pZCI6IjEifQ...
Что происходит
# Псевдокод
def auth_current_user() -> User | None:
user = current_user
if user.is_anonymous or not user.is_active:
return None
return user
current_user — это объект из flask_login, который декодирует cookie и lookup user. Cookie cached, поэтому DB lookup случается только при первом запросе сессии — далее быстро.
Когда использовать
- AJAX из UI (это default behavior, ничего настраивать не нужно)
- Internal dashboards, которые шарят домен с Airflow webserver (cookie передаётся)
НЕ использовать для machine-to-machine
Machine не может выполнить login flow с CSRF и cookie storage — session backend для них непригоден.
Backend 3: kerberos_auth
Корпоративный SSO через Kerberos / SPNEGO. Используется в enterprise deployments с Active Directory.
[api]
auth_backends = airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth
Поток
Client request → 401 Unauthorized + WWW-Authenticate: Negotiate
Client → kinit получает TGT → service ticket → отправляет в Authorization: Negotiate <token>
Server → валидирует token через keytab → username
Конфигурация:
[kerberos]
keytab = /etc/airflow/airflow.keytab
principal = airflow/[email protected]
ccache = /tmp/airflow_krb5_ccache
Kerberos требует правильно настроенного DNS (forward + reverse), синхронизированных часов (NTP, расхождение > 5 минут ломает auth), и корректного SPN в keytab. Это самый сложный backend в setup, но даёт seamless SSO для enterprise users.
Backend 4: Custom (включая JWT)
Airflow позволяет писать свой auth backend через Python module. Это нужно для:
- JWT (через Auth0, AWS Cognito, Okta) — JWT не входит в коробку в 2.x
- mTLS — client certificate based
- API key (простой
X-API-Keyheader) - HMAC signing
Скелет custom backend
# airflow_plugins/jwt_auth.py
from functools import wraps
from typing import Callable, TypeVar, Sequence
import jwt
from flask import Response, request, g
from airflow.www.fab_security.sqla.manager import SecurityManager
CLIENT_AUTH = None # required attribute (для outbound auth)
PUBLIC_KEY = open("/etc/airflow/jwt_public.pem").read()
T = TypeVar("T", bound=Callable)
def init_app(_):
"""Hook вызываемый при старте webserver."""
pass
def requires_authentication(function: T) -> T:
@wraps(function)
def decorated(*args, **kwargs):
header = request.headers.get("Authorization", "")
if not header.startswith("Bearer "):
return Response("Unauthorized", 401, {"WWW-Authenticate": "Bearer"})
token = header[len("Bearer "):]
try:
claims = jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"], audience="airflow")
except jwt.PyJWTError as e:
return Response(f"Invalid token: {e}", 401)
# Mapping JWT claim → Airflow user
username = claims.get("preferred_username") or claims.get("sub")
sm: SecurityManager = current_app.appbuilder.sm
user = sm.find_user(username=username)
if user is None:
# Auto-provision user из JWT claims
user = sm.add_user(
username=username,
first_name=claims.get("given_name", ""),
last_name=claims.get("family_name", ""),
email=claims["email"],
role=sm.find_role(map_jwt_role_to_airflow(claims.get("roles", []))),
)
g.user = user
return function(*args, **kwargs)
return decorated
Регистрация:
[api]
auth_backends = airflow_plugins.jwt_auth
Файл должен быть в PYTHONPATH на webserver pod.
Что важно при custom backend
CLIENT_AUTHатрибут обязателен — даже если используется только для server-side. Без него Airflow падает при старте.- Auto-provisioning users — JWT не имеет встроенного registration flow в Airflow. Backend сам решает: либо требовать pre-created user (с матчингом по username), либо создавать на лету.
- Role mapping — JWT claims → Airflow roles. Обычно через config map.
FAB permissions — авторизация после аутентификации
Auth backend отвечает «кто это?». Permissions отвечают «что им можно?». Это две разные стадии.
# Внутри endpoint Airflow проверяет:
@security.requires_access(
permissions=[(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_RUN)]
)
def get_dag_runs(...):
...
Permissions хранятся в FAB tables:
SELECT
u.username,
r.name AS role,
p.name AS permission,
v.name AS resource
FROM ab_user u
JOIN ab_user_role ur ON u.id = ur.user_id
JOIN ab_role r ON ur.role_id = r.id
JOIN ab_permission_view_role pvr ON r.id = pvr.role_id
JOIN ab_permission_view pv ON pvr.permission_view_id = pv.id
JOIN ab_permission p ON pv.permission_id = p.id
JOIN ab_view_menu v ON pv.view_menu_id = v.id
WHERE u.username = 'ci_user'
ORDER BY resource, permission;
Built-in roles
| Role | Что можно | Когда использовать |
|---|---|---|
| Public | Ничего (auth обязательна) | — |
| Viewer | Read DAGs/runs/logs | Read-only dashboards |
| User | Viewer + trigger/clear | Power users |
| Op | User + manage connections/variables/pools | DevOps |
| Admin | Всё | Super-admin |
DAG-level permissions (с 2.x)
С версии 2.0 permissions могут быть per-DAG. Это позволяет multi-tenant deployments:
-- Team A может только читать свои DAGs
SELECT * FROM ab_view_menu WHERE name LIKE 'DAG:team_a_%';
Resource named DAG:my_dag_id означает разрешение конкретно на этот DAG. CI service account можно ограничить только своими DAGs:
airflow users create \
--username ci_team_a \
--role TeamA_CI \
--firstname CI \
--lastname Team A \
--email [email protected] \
--password ...
airflow roles add-perms TeamA_CI \
--action can_read --resource "DAG:team_a_*"
Rate limiting
С 2.5+ Airflow имеет встроенный rate limiter (Flask-Limiter):
[api]
auth_rate_limited = True
auth_rate_limit = 5 per 40 seconds
[webserver]
rate_limit_for_authenticated = 100 per minute
auth_rate_limit — глобальный per-IP лимит на попытки login (защита от brute-force). НЕ ограничивает успешные запросы.
Для production высоконагруженных integration используйте rate limit на уровне reverse proxy (NGINX limit_req_zone, AWS API Gateway throttling) — это даёт более тонкий контроль и не нагружает webserver.
Сравнение auth backends
| Backend | Latency overhead | DB lookups | Use case | Setup сложность |
|---|---|---|---|---|
basic_auth | 80-120ms (bcrypt) | каждый request | Manual, CI/CD low-volume | Низкая |
session | <1ms (cookie cached) | первый request | UI AJAX | Default |
kerberos_auth | 5-15ms (token validate) | первый request | Enterprise SSO | Высокая |
| Custom JWT | 1-5ms (signature verify) | первый request (provision) | M2M, OAuth integrations | Средняя |
aws_auth_manager (provider) | 10-50ms (IAM call) | каждый request | MWAA, AWS-native | Средняя |
Production gotchas
1. basic_auth + bcrypt — webserver CPU bottleneck
Бенчмарк: 4 vCPU webserver с basic_auth обслуживает ~30-50 RPS auth requests, потом bcrypt становится bottleneck. Симптомы: 99% CPU на gunicorn workers, latency UI растёт.
Fix: для high-volume использовать JWT (sub-millisecond verify) или session cookie.
2. CSRF token блокирует POST
Если включена FAB CSRF protection (WTF_CSRF_ENABLED = True), все POST/PATCH/DELETE требуют CSRF token. Для API это не нужно — отключить:
[webserver]
WTF_CSRF_ENABLED = False # но это влияет и на UI forms!
Лучше — exempt только API:
# webserver_config.py
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect()
def init_app(app):
csrf.init_app(app)
csrf.exempt(api_v1_blueprint)
3. [api] auth_backends влияет на public endpoints
Endpoint /health, /api/v1/version — публичные. Но если включён deny_all backend, они тоже блокируются. Tip: использовать airflow.api.auth.backend.deny_all в production когда API не нужен (но UI работает через FAB напрямую).
4. Anonymous DAG triggers (legacy)
В 1.x был параметр [api] auth_backend = airflow.api.auth.backend.default — anonymous access. Это deprecated и опасно. Никогда не используйте в production.
5. SSO + JWT — auto-provisioning permissions
Custom backend auto-creates user, но роль присваивается какая? По default — Public (без permissions) → пользователь авторизован, но ничего не может. Fix: явно mapping JWT claims roles → Airflow roles в backend.