JWT 인증 (1) REST API 흐름과 서명 방식
Apache Airflow Official Documentation (in-repo snapshot) — Apache Software Foundation security/jwt_token_authentication.rst — Overview, Signing and Cryptography, REST API Authentication Flow 섹션
이 모듈을 다 읽으면
- 대칭(HS512)과 비대칭(RS256/EdDSA) 서명 방식의 차이와 각각의 키 노출 위험을 설명할 수 있다
- REST API 토큰의 발급 절차, claim 구성, 검증 순서를 설명할 수 있다
- 토큰 폐기(revocation)와 갱신(refresh)이 REST API 토큰에만 적용되는 이유를 판단할 수 있다
Airflow 3는 JWT를 REST API(Core API)와 내부 Execution API 양쪽의 인증 수단으로 쓴다. 두 흐름은 같은 JWTGenerator/JWTValidator 인프라를 공유하지만 audience, 토큰 수명, subject claim, scope 의미가 다르다. 이 모듈은 REST API 흐름과 그 바탕이 되는 서명 방식을 다룬다.
두 가지 JWT 흐름과 서명 인프라 공유
Airflow의 API 인증에는 두 가지 구분되는 JWT 흐름이 있다. 하나는 UI 사용자, CLI 도구, 외부 클라이언트가 Public API(Core API)와 통신할 때 쓰는 REST API 흐름이고, 다른 하나는 워커, Dag File Processor, Triggerer가 태스크 상태를 보고하고 커넥션/변수/XCom 같은 런타임 데이터를 가져올 때 쓰는 Execution API 흐름이다.
두 흐름 모두 airflow.api_fastapi.auth.tokens의 JWTGenerator와 JWTValidator 클래스를 공유하지만, audience(aud) claim, 토큰 수명, subject(sub) claim의 의미, scope의 해석 방식이 서로 다르다. 예를 들어 UI/CLI/외부 클라이언트는 API Server에 Bearer 토큰이나 쿠키로 접근하고, 워커는 workload에서 execution으로 전환되는 Bearer 토큰으로 Execution API에 접근하는 반면, Dag File Processor와 Triggerer는 in-process 방식으로 JWT 검증 자체를 우회한다.
핵심 포인트
- REST API와 Execution API는 같은 JWTGenerator/JWTValidator 인프라를 공유하지만 audience, 토큰 수명, subject claim, scope 의미가 다르다
- UI/CLI/외부 클라이언트는 API Server에, 워커는 Execution API에 각각 다른 성격의 JWT로 접근한다
서명 방식: 대칭키(HS512) vs 비대칭키(RS256/EdDSA)
Airflow는 상호 배타적인 두 가지 서명 모드를 지원한다. 대칭(symmetric) 모드는 [api_auth] jwt_secret으로 지정한 사전 공유 비밀키와 HS512 알고리즘을 사용하며, 토큰을 생성하거나 검증하는 모든 컴포넌트가 동일한 비밀키를 가져야 한다. 비밀키를 설정하지 않으면 Airflow는 시작 시 무작위 16바이트 키를 자동 생성하는데, 이 키는 프로세스마다 다르고 재시작 때마다 바뀌는 임시 키이므로 다중 컴포넌트 배포에서는 인증 실패를 유발한다. 따라서 Deployment Manager는 이 값을 반드시 명시적으로 설정해야 한다.
비대칭(asymmetric) 모드는 [api_auth] jwt_private_key_path로 지정한 PEM 인코딩 개인키로 서명하고, 대응하는 공개키로 검증한다. 지원 알고리즘은 RSA 키에는 RS256, Ed25519 키에는 EdDSA이며, [api_auth] jwt_algorithm이 기본값 GUESS일 때는 키 타입에서 알고리즘을 자동 감지한다. 검증 시에는 [api_auth] trusted_jwks_url로 지정한 JWKS(로컬 파일 또는 원격 HTTP/HTTPS, 주기적으로 폴링)를 쓰거나, trusted_jwks_url이 없으면 설정된 개인키에서 유도한 공개키로 자동 폴백한다.
두 모드의 근본적인 차이는 키 노출 범위에 있다. 비대칭 모드에서는 검증자(워커, 다운스트림 서비스)가 공개키만 있으면 되므로 서명용 개인키를 발급 컴포넌트(API Server, Scheduler)에만 좁게 배치할 수 있다. 반면 대칭 모드에서는 키가 하나뿐이므로, 토큰을 검증할 수 있는 모든 컴포넌트가 곧 토큰을 위조할 수도 있다는 뜻이 된다.
핵심 포인트
- jwt_secret을 설정하지 않으면 프로세스마다 다른 임시 16바이트 키가 자동 생성되어 다중 컴포넌트 배포에서 인증 실패를 유발하므로 반드시 명시적으로 설정해야 한다
- jwt_algorithm 기본값 GUESS는 키 타입에 따라 서명 알고리즘을 자동 감지한다(대칭=HS512, RSA=RS256, Ed25519=EdDSA)
- 비대칭 모드는 검증자에게 공개키만 배포하면 되어 개인키 노출 범위를 좁힐 수 있지만, 대칭 모드는 검증 가능한 컴포넌트가 곧 위조도 가능하다
REST API 토큰 발급과 구조
REST API 토큰은 클라이언트가 자격증명을 담아 POST /auth/token을 호출하면, auth manager가 자격증명을 검증해 사용자 객체를 만들고 이를 JWT claim으로 직렬화해 JWTGenerator.generate()를 호출하는 방식으로 발급된다. 생성된 토큰은 응답의 access_token 필드로 반환된다.
UI 기반 인증에서는 토큰이 SameSite=Lax인 HttpOnly 보안 쿠키(_token)에 저장된다. CLI는 별도의 엔드포인트(/auth/token/cli)를 사용하며 이 토큰은 REST API 토큰보다 짧은 만료 시간을 갖는다.
토큰에는 jti(폐기 추적용 UUID4), iss(발급자), aud(대상), sub(사용자 식별자), iat(발급 시각), nbf(iat와 동일), exp(iat + jwt_expiration_time) claim이 포함된다.
핵심 포인트
- UI 토큰은 SameSite=Lax인 HttpOnly _token 쿠키에 저장되고, CLI는 별도 엔드포인트(/auth/token/cli)에서 더 짧은 만료시간의 토큰을 받는다
- REST API 토큰의 claim은 jti/iss/aud/sub/iat/nbf/exp로 구성되며, jti는 폐기 추적에 쓰인다
토큰 검증 순서, 폐기, 갱신
API 요청마다 토큰은 Authorization: Bearer 헤더, OAuth2 쿼리 파라미터, _token 쿠키 순서로 추출을 시도한다. JWTValidator는 서명, 만료(exp), not-before(nbf), issued-at(iat), audience, issuer를 검증하며, [api_auth] jwt_leeway(기본 10초)로 컴포넌트 간 시계 오차를 흡수한다.
토큰 폐기는 REST API/UI 토큰에만 적용되고 Execution API 토큰에는 쓰이지 않는다. 폐기된 토큰은 jti 값으로 revoked_token 테이블에 기록되며, 로그아웃이나 명시적 폐기 시 jti와 exp가 삽입된다. 만료된 항목은 2 × jwt_expiration_time 주기로 자동 정리된다. /auth/logout 엔드포인트는 리다이렉트나 쿠키 삭제 이전에 항상 auth_manager.revoke_token()을 무조건 호출한다 — 이는 FabAuthManager나 KeycloakAuthManager처럼 외부 로그아웃 URL로 리다이렉트하는 배포에서도 마찬가지이며, 외부 IdP가 자신의 세션을 어떻게 처리하든 Airflow의 revoked_token 테이블에는 폐기 기록이 남는다. 서버 측 폐기를 구현하지 않는 auth manager는 기본 no-op 구현을 그대로 쓸 수 있다.
토큰 갱신은 JWTRefreshMiddleware가 UI 요청에서 처리한다. 현재 _token 쿠키가 만료에 가까워지면 auth_manager.refresh_user()를 호출해 새 토큰을 생성하고 갱신된 쿠키로 교체한다.
핵심 포인트
- 토큰 추출 우선순위는 Authorization 헤더 > OAuth2 쿼리 파라미터 > _token 쿠키 순이다
- 토큰 폐기는 REST API/UI 토큰에만 적용되며, /auth/logout은 외부 IdP 리다이렉트 여부와 무관하게 항상 revoke_token()을 호출한다
- JWTRefreshMiddleware는 UI 요청에서 _token 쿠키 만료가 임박하면 자동으로 새 토큰을 발급해 쿠키를 갱신한다