JWT 인증 아키텍처와 컨트롤플레인/워커플레인 신뢰 경계
Apache Airflow Official Documentation (in-repo snapshot) — Apache Software Foundation security/security_model.rst - "JWT authentication and workload isolation" 섹션, mermaid 신뢰 경계 다이어그램과 "Defense in depth at the router level" (약 352-431행)
이 모듈을 다 읽으면
- 워커 플레인과 컨트롤 플레인이 메타데이터 DB 접근 여부를 기준으로 어떻게 나뉘는지 설명할 수 있다
- Dag File Processor/Triggerer가 사용자 코드를 실행함에도 컨트롤 플레인 신뢰 경계에 포함되는 이유를 설명할 수 있다
- 라우터 레벨 Depends(get_user) 선언이 실제 인증 로직이 아니라 회귀 방지용 방어임을 설명할 수 있다
Airflow는 공개 REST API와 내부 Execution API 모두에 JWT 인증을 사용한다. 컴포넌트들은 메타데이터 DB에 대한 접근 여부로 워커 플레인과 컨트롤 플레인으로 나뉘는데, 이 구분은 인증 방식이 아니라 DB 접근 여부를 기준으로 하기 때문에, 사용자 코드를 실행하는 Dag File Processor/Triggerer도 컨트롤 플레인에 속하는 비대칭 구조가 생긴다.
신뢰 경계 다이어그램: 워커 플레인 vs 컨트롤 플레인
Airflow는 공개 REST API와 내부 Execution API 모두에 JWT(JSON Web Token) 인증을 사용한다. 문서가 제시하는 mermaid 다이어그램은 Airflow 컴포넌트들과 메타데이터 DB 사이의 신뢰 경계를 요약한다: 실선 화살표는 인증된 네트워크 호출(JWT를 실은 HTTP)이고, 점선 화살표는 직접 데이터베이스 접근이다 - 점선 오른쪽에 있는 컴포넌트는 메타데이터 DB를 읽고 쓸 수 있으므로 그 컴포넌트가 실행하는 어떤 코드든 암묵적으로 메타데이터 DB에 대해 신뢰받는 셈이다.
UI/CLI/외부 REST 클라이언트, 그리고 워커는 모두 JWT를 통해 API Server에 인증한다(워커는 Execution API 경유). API Server, Scheduler, Dag File Processor, Triggerer는 모두 SQL로 메타데이터 DB에 직접 접근한다 - 이들이 '컨트롤 플레인'이다. 반면 워커('워커 플레인')는 메타데이터 DB에 대한 접근 권한이 없다.
의도된 비대칭성은 다음과 같다: 워커는 DB 자격증명을 전혀 갖지 않고 JWT로 인증된 Execution API를 통해서만 데이터에 접근하는 반면, Dag File Processor와 Triggerer는 컨트롤 플레인의 DB 접근을 공유하면서도 JWT bearer 의존성을 우회하는 in-process 전송을 사용한다. 이것이 Dag File Processor와 Triggerer가 사용자 제공 코드를 실행함에도 불구하고 컨트롤 플레인 신뢰 경계의 일부로 취급되는 이유다.
핵심 포인트
- 실선은 JWT 인증 네트워크 호출, 점선은 직접 DB 접근을 의미하며, 점선 오른쪽 컴포넌트는 암묵적으로 DB에 대해 신뢰된다
- API Server, Scheduler, Dag File Processor, Triggerer는 모두 메타데이터 DB에 SQL로 직접 접근하는 '컨트롤 플레인'이다
- 워커는 DB 자격증명이 전혀 없고 JWT 인증된 Execution API로만 데이터에 접근하는 '워커 플레인'이다
- Dag File Processor/Triggerer는 사용자 코드를 실행하지만 DB 접근을 공유하고 JWT 우회 in-process 전송을 쓰기 때문에 컨트롤 플레인으로 분류된다
라우터 레벨 심층 방어(Defense in depth)
공개 REST API 라우터(``/api/v2``)와 UI 라우터(``/ui``)는 모두 라우터 레벨에서 ``Depends(get_user)``를 선언한다. 이는 순수하게 심층 방어(defense-in-depth) 백스톱이다: 이미 모든 인증된 라우트가 자체적으로 ``GetUserDep`` 또는 ``requires_access_*`` 의존성을 선언하고 있고, 그 의존성 자체가 ``get_user``를 해석하며, FastAPI는 요청당 의존성 해석을 캐시하므로 중복 선언은 한 번만 해석되고 런타임 비용은 0이다.
이 라우터 레벨 선언의 진짜 가치는, 향후 누군가 인증 체크 없이 두 라우터 중 하나에 새 라우트를 추가하는 것을 막는 데 있다 - 라우터 레벨 의존성이 등록 시점에 그 회귀를 잡아낸다. 명시적인 무인증 예외는 헬스 프로브(``monitor_router``), ``version_router``, 그리고 로그인 엔드포인트를 포함하는 공개 ``auth_router``로 한정되며, 이들은 ``authenticated_router`` 하위가 아니라 공개 라우터에 직접 마운트된다.
구조적 테스트가 두 라우터 모두 라우터 레벨 ``Depends(get_user)``를 갖는지 단언하므로, 이 목적을 고려하지 않고 의존성을 제거하는 리팩터링은 조용히 무인증 표면을 넓히는 대신 CI에서 실패한다.
핵심 포인트
- 라우터 레벨 Depends(get_user)는 이미 각 라우트가 갖고 있는 인증 의존성과 중복이며 FastAPI 캐싱 덕분에 런타임 비용이 없다
- 이 선언의 실질 가치는 인증 체크 없는 새 라우트 추가를 등록 시점에 잡아내는 회귀 방지 백스톱이다
- 무인증 예외는 monitor_router(헬스 프로브), version_router, 공개 auth_router(로그인)로 한정된다
- 구조적 테스트가 이 의존성 선언 여부를 검증해 실수로 제거되면 CI가 실패하도록 되어 있다