JWT 토큰 인증

JWT 토큰 인증 (JWT Token Authentication)

이 문서는 Apache Airflow에서 JWT(JSON Web Token) 인증이 어떻게 동작하는지 설명해요. 공개 REST API(Core API)와 워커가 사용하는 내부 Execution API 두 가지 흐름을 모두 다룹니다.

출처: 문서

본문

이 문서는 Apache Airflow에서 공개 REST API(Core API)와 워커가 사용하는 내부 Execution API 양쪽에서 JWT(JSON Web Token) 인증이 어떻게 동작하는지 설명해요.

개요 (Overview)

Airflow는 API의 기본 인증 메커니즘으로 JWT 토큰을 사용해요. 두 가지 뚜렷한 JWT 인증 흐름이 있어요:

  1. REST API (Core API) — UI 사용자, CLI 도구, 외부 클라이언트가 Airflow 공개 API와 상호작용하는 데 사용.
  2. Execution API — 워커, Dag File Processor, Triggerer가 task 상태를 통신하고 런타임 데이터(connections, variables, XComs)를 가져오는 데 내부적으로 사용.

두 흐름 모두 동일한 기본 JWT 인프라(airflow.api_fastapi.auth.tokensJWTGeneratorJWTValidator 클래스)를 공유하지만, audience, 토큰 수명, subject 클레임, scope 의미론에서 차이가 있어요.

        flowchart LR
    subgraph Clients
        UI[UI / browser]
        CLI[CLI]
        EXT[External REST clients]
    end
    subgraph Internal["Internal Airflow components"]
        WORKER[Worker / Task]
        DFP[Dag File Processor]
        TRG[Triggerer]
    end
    APISVR[API Server]
    EXECAPI[Execution API]
    UI -->|JWT cookie / Bearer| APISVR
    CLI -->|Bearer| APISVR
    EXT -->|Bearer| APISVR
    WORKER -->|Bearer<br/>workload &rarr; execution| EXECAPI
    DFP -. in-process<br/>JWT bypassed .-> EXECAPI
    TRG -. in-process<br/>JWT bypassed .-> EXECAPI

    classDef internal fill:#bbdefb,stroke:#1565c0,stroke-width:2px,color:#000
    class WORKER,DFP,TRG internal

서명과 암호화 (Signing and Cryptography)

Airflow는 상호 배타적인 두 가지 서명 모드를 지원해요:

대칭 (공유 비밀) : 사전 공유된 비밀 키([api_auth] jwt_secret)와 HS512 알고리즘을 사용해요. 토큰을 생성하거나 검증하는 모든 컴포넌트는 같은 비밀을 공유해야 해요. 비밀이 구성되지 않으면 Airflow는 시작 시 무작위 16바이트 키를 자동 생성해요 — 하지만 이 키는 임시적이고 프로세스마다 달라서, 다중 컴포넌트 배포에서는 인증 실패를 일으킬 거예요. 배포 관리자(Deployment Managers)는 이 값을 명시적으로 구성해야 해요.

비대칭 (공개/개인 키 쌍) : 서명에는 PEM 인코딩된 개인 키([api_auth] jwt_private_key_path), 검증에는 해당 공개 키를 사용해요. 지원 알고리즘: RS256 (RSA) 및 EdDSA (Ed25519). [api_auth] jwt_algorithmGUESS(기본값)로 설정되면 알고리즘이 키 유형에서 자동 감지돼요.

검증은 다음 중 하나를 사용할 수 있어요:

- `[api_auth] trusted_jwks_url`로 구성된 JWKS(JSON Web Key Set) 엔드포인트 (로컬 파일 또는 원격 HTTP/HTTPS URL, 업데이트를 위해 주기적으로 폴링).
- 구성된 개인 키에서 파생된 공개 키 (`trusted_jwks_url`이 설정되지 않았을 때 자동 폴백).
        flowchart TB
    subgraph Sym["Symmetric (HS512)"]
        direction LR
        S1[Scheduler / API Server]
        S2[Shared secret<br/>jwt_secret]
        S3[Token validator]
        S1 -->|sign| S2 -->|same secret<br/>also validates| S3
    end
    subgraph Asym["Asymmetric (RS256 / EdDSA)"]
        direction LR
        A1[Scheduler / API Server]
        A2[Private key<br/>jwt_private_key_path]
        A3[Public key /<br/>JWKS endpoint]
        A4[Token validator]
        A1 -->|sign| A2
        A2 -. derives or<br/>publishes .-> A3
        A3 -->|verify only| A4
    end

    classDef secret fill:#ffcdd2,stroke:#c62828,stroke-width:2px,color:#000
    classDef pub fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px,color:#000
    class S2 secret
    class A2 secret
    class A3 pub

비대칭 모드에서 검증자(워커, 다운스트림 서비스)는 공개 키만 필요해요 — 개인 서명 키는 발급 컴포넌트(API Server, Scheduler)에 엄격하게 한정할 수 있죠. 대칭 모드에서는 키가 하나뿐이므로 토큰을 검증할 수 있는 모든 컴포넌트가 토큰을 위조할 수도 있어요. 배포 영향은 JWT 인증과 워크로드 격리를 참고하세요.

REST API 인증 흐름

토큰 획득 (Token acquisition)

  1. 클라이언트가 자격 증명(예: JSON body의 username과 password)으로 /auth/tokenPOST 요청을 보내요.
  2. auth manager가 자격 증명을 검증하고 user 객체를 만들어요.
  3. auth manager가 user를 JWT 클레임으로 직렬화하고 JWTGenerator.generate()를 호출해요.
  4. 생성된 토큰이 access_token으로 응답에 반환돼요.

UI 기반 인증의 경우 토큰은 SameSite=Lax가 있는 보안 HTTP-only 쿠키(_token)에 저장돼요.

CLI는 다른(더 짧은) 만료 시간을 가진 별도 엔드포인트(/auth/token/cli)를 사용해요.

토큰 구조 (REST API)

Claim Description
jti 고유 토큰 식별자 (UUID4 hex). 토큰 취소에 사용.
iss 발급자 ([api_auth] jwt_issuer에서).
aud 수신자 audience ([api_auth] jwt_audience에서).
sub 사용자 식별자 (auth manager가 직렬화).
iat 발급 시각 (Unix epoch 초).
nbf not-before 타임스탬프 (iat와 동일).
exp 만료 타임스탬프 (iat + jwt_expiration_time).

토큰 검증 (REST API)

각 API 요청에서 토큰은 다음 우선순위 순서로 추출돼요:

  1. `Authorization: Bearer *** 헤더.
  2. OAuth2 쿼리 파라미터.
  3. _token 쿠키.

JWTValidator는 서명, 만료(exp), not-before(nbf), 발급 시각(iat), audience, 발급자 클레임을 검증해요. 구성 가능한 leeway([api_auth] jwt_leeway, 기본 10초)가 클록 스큐를 보정해요.

토큰 취소 (REST API만 해당)

토큰 취소는 REST API와 UI 토큰에만 적용돼요 — 워커에 발급된 Execution API 토큰에는 사용되지 않아요.

취소된 토큰은 jti 클레임으로 revoked_token 데이터베이스 테이블에 추적돼요. 로그아웃 또는 명시적 취소 시 토큰의 jtiexp가 이 테이블에 삽입돼요. 만료된 항목은 2× jwt_expiration_time의 주기로 자동 정리돼요.

/auth/logout 엔드포인트는 모든 리디렉션 또는 쿠키 삭제 전에 항상 auth_manager.revoke_token()을 호출해요. 여기에는 구성된 auth manager(예: FabAuthManager 또는 KeycloakAuthManager)가 사용자를 외부 로그아웃 URL로 리디렉션하는 배포도 포함돼요 — 외부 Identity Provider가 자체 세션으로 무엇을 하든 JWT는 Airflow의 revoked_token 테이블에서 무효화돼요. revoke_token 호출은 무조건적이며, 서버 측 취소를 구현하지 않는 auth manager는 기본 no-op 구현을 유지할 수 있어요.

토큰 갱신 (REST API)

JWTRefreshMiddleware가 UI 요청에서 실행돼요. 미들웨어는 현재 토큰의 _token 쿠키가 만료에 가까워지는 것을 감지하면 auth_manager.refresh_user()를 호출해 새 토큰을 생성하고 업데이트된 쿠키로 설정해요.

기본 타이밍 (REST API)

Setting Default
[api_auth] jwt_expiration_time 86400 초 (24시간)
[api_auth] jwt_cli_expiration_time 3600 초 (1시간)
[api_auth] jwt_leeway 10 초

Execution API 인증 흐름

Execution API는 Airflow 자체가 사용하는 API로(제3자 호출자가 아니라) task 상태 전이를 보고·설정하고, heartbeat를 보내고, task 런타임에 connections·variables·XComs를 검색하고, 실행 및 Dag 파싱을 트리거하는 데 사용돼요.

토큰 생성 (Execution API)

  1. Scheduler가 각 task 인스턴스에 대해,(executor를 통해) 워커로 디스패치하기 전에 JWT를 생성해요. executor의 jwt_generator 프로퍼티가 [execution_api] 설정으로 구성된 JWTGenerator를 만들어요.
  2. 토큰의 sub(subject) 클레임이 task instance UUID로 설정돼요.
  3. 토큰이 워커 프로세스로 보내지는 워크로드 JSON 페이로드(BaseWorkloadSchema.token 필드)에 포함돼요.

토큰 구조 (Execution API)

Claim Description
jti 고유 토큰 식별자 (UUID4 hex).
iss 발급자 ([api_auth] jwt_issuer에서).
aud Audience ([execution_api] jwt_audience, 기본: urn:airflow.apache.org:task).
sub Task instance UUID — 워크로드의 신원.
scope 토큰 스코프: "execution" 또는 "workload".
iat 발급 시각.
nbf not-before 타임스탬프.
exp 만료 타임스탬프 (iat + [execution_api] jwt_expiration_time).

토큰 스코프 (Execution API)

Execution API는 수명이 다른 두 가지 토큰 스코프를 정의해요:

workload : Scheduler가 task를 디스패치할 때 워크로드 JSON 페이로드에 포함되는 토큰. 더 긴 수명으로 task가 실행 시작 전 executor 큐에서 대기하는 동안 유효하게 유지돼요. 워커가 workload 토큰으로 /run 엔드포인트를 호출하면, 서버가 Refreshed-API-Token 응답 헤더에 새 execution-스코프 토큰을 발급해요. 수명은 [scheduler] task_queued_timeout(기본 600초)과 같아요 — 큐에 갇힌 task를 스케줄러가 수거할 때 사용하는 것과 같은 타임아웃이죠 — 따라서 task_queued_timeout를 조정하면 task가 backlog된 큐에서 workload 토큰이 만료되기 전에 대기할 수 있는 창도 넓어져요.

execution : 모든 Execution API 엔드포인트가 허용하는 수명이 짧은 토큰(기본 10분). task 실행 중 워커 통신의 표준 스코프예요. 워커가 /run 엔드포인트를 통해 running으로 전환할 때 서버가 발급해요. JWTReissueMiddlewareexecution 토큰을 투명하게 갱신하므로 워커는 task 기간 동안 접근을 유지해요.

scope 클레임이 없는 토큰은 역호환성을 위해 "execution"으로 기본 설정돼요.

워커로의 토큰 전달

토큰은 다음과 같이 실행 스택을 통해 흘러가요:

  1. Schedulerworkload-스코프 토큰(수명은 [scheduler] task_queued_timeout, 기본 600초)을 생성해 Executor로 전달하는 워크로드 JSON 페이로드에 포함해요.
  2. 워크로드 JSON이 (executor별 메커니즘: Celery 메시지, Kubernetes Pod spec, 로컬 하위 프로세스 인자 등) 워커 프로세스로 전달돼요.
  3. 워커의 execute_workload() 함수가 워크로드 JSON을 읽고 토큰을 추출해요.
  4. supervise_task() 함수가 토큰을 받아 모든 Execution API HTTP 요청에 BearerAuth(token)을 사용하는 httpx.Client 인스턴스를 만들어요.
  5. 워커가 workload-스코프 토큰으로 /run 엔드포인트를 호출해 task를 running으로 표시해요. 서버는 Refreshed-API-Token 헤더에 새 execution-스코프 토큰으로 응답해요.
  6. 클라이언트의 _update_auth() 훅이 헤더를 감지하고 BearerAuth 인스턴스를 투명하게 업데이트해 이후 모든 요청에 새 execution 토큰을 사용해요.
        sequenceDiagram
    autonumber
    participant SCH as Scheduler
    participant EXE as Executor<br/>(Celery / K8s / Local)
    participant WRK as Worker
    participant API as Execution API

    Note over SCH: Task ready to dispatch
    SCH->>SCH: generate workload token<br/>scope=workload<br/>exp = task_queued_timeout
    SCH->>EXE: workload JSON<br/>(includes token)
    Note over EXE: Task waits in queue<br/>(can be minutes)
    EXE->>WRK: dispatch (workload JSON)
    WRK->>API: PATCH /run<br/>Bearer: workload token
    Note over API: validates workload scope<br/>checks TI in QUEUED/RESTARTING<br/>409 if not
    API-->>WRK: 200 OK<br/>Refreshed-API-Token: execution token<br/>(scope=execution, ~10 min)
    WRK->>WRK: BearerAuth swaps to<br/>execution token
    loop For all subsequent calls (heartbeats, XComs, ...)
        WRK->>API: Bearer: execution token
        alt token expiring (less than 20% left)
            API-->>WRK: 200 OK<br/>Refreshed-API-Token: new execution token
            WRK->>WRK: BearerAuth swaps again
        end
    end

workload 토큰이 전송 중에 가로채여도 /run만 호출할 수 있어요. 그 엔드포인트는 재실행을 거부하고(task instance가 QUEUED 또는 RESTARTING이 아니면 409 Conflict), 따라서 더 긴 수명 토큰의 공격 표면은 "이미 큐에 있는 task 시작"으로 제한돼요. 다른 모든 엔드포인트는 scope=execution을 요구하고 workload 토큰을 거부해요.

토큰 검증 (Execution API)

JWTBearer 보안 의존성이 요청당 한 번 토큰을 검증해요:

  1. `Authorization: *** 헤더에서 토큰을 추출해요.
  2. JWTValidator로 암호화 서명 검증을 수행해요.
  3. 표준 클레임(exp, iat, aud — 구성된 경우 nbfiss)을 확인해요.
  4. scope 클레임이 없으면 "execution"으로 기본 설정해요.
  5. task 신원 클레임을 타입이 지정된 Pydantic 스키마에 대해 검증해요. TIClaims(airflow.api_fastapi.execution_api.datamodels.token에 있음)는 scope가 선언된 TokenScope 리터럴("execution" 또는 "workload") 중 하나인지 강제해요. 그런 다음 TITokenUUID 필드를 통해 sub 클레임을 파싱하므로 비 UUID 값을 거부해요. scope가 알 수 없거나 sub가 유효한 UUID가 아닌 토큰은, 암호화 서명 검사가 통과하더라도 403 Forbidden으로 거부돼요. TIClaimsextra="allow"를 유지하므로 auth manager가 코어 스키마를 수정하지 않고 추가로 배포 특정 클레임을 첨부할 수 있어요. 보안에 중요한 필드만 타입이 지정돼요.
  6. task instance ID와 검증된 클레임으로 TIToken 객체를 만들어요.
  7. 검증된 토큰을 요청 기간 동안 ASGI 요청 스코프에 캐시해요.

라우트 수준 강제는 require_auth가 처리해요:

  • 토큰의 scope를 라우트의 allowed_token_types(라우트 등록 시 ExecutionAPIRoutetoken:* Security 스코프에서 미리 계산)와 확인해요.
  • ti:self 스코프를 강제해요 — 토큰의 sub 클레임이 {task_instance_id} 경로 파라미터와 일치하는지 확인해 워커가 다른 task의 엔드포인트에 접근하지 못하게 해요.
        flowchart TD
    REQ([Incoming request<br/>Authorization: Bearer ***
    REQ --> CACHE{Cached on<br/>request.scope?}
    CACHE -->|yes| RET([Return cached TIToken])
    CACHE -->|no| SIG[JWTValidator:<br/>verify signature]
    SIG -->|fail| F1([403 Forbidden])
    SIG -->|ok| STD[Verify exp / iat / nbf<br/>aud / iss]
    STD -->|fail| F1
    STD -->|ok| SCOPE[Default scope to<br/>'execution' if absent]
    SCOPE --> SCHEMA[TIClaims:<br/>typed Pydantic schema]
    SCHEMA -->|ValidationError| F1
    SCHEMA -->|ok| TYP{require_auth:<br/>scope in<br/>route.allowed_token_types?}
    TYP -->|no| F1
    TYP -->|yes| SELF{ti:self scope<br/>declared?}
    SELF -->|no| OK([Return TIToken])
    SELF -->|yes| MATCH{token.sub ==<br/>task_instance_id?}
    MATCH -->|no| F1
    MATCH -->|yes| OK

    classDef fail fill:#ffcdd2,stroke:#c62828,stroke-width:2px,color:#000
    classDef pass fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px,color:#000
    class F1 fail
    class OK,RET pass

토큰 갱신 (Execution API)

JWTReissueMiddleware가 만료에 가까워지는 유효한 토큰을 자동으로 갱신해요. 갱신이 발생하려면 요청 시작 시점에 토큰이 유효해야 해요:

  1. 각 응답 후 미들웨어가 토큰의 남은 유효 시간을 확인해요.
  2. 전체 유효 시간의 20% 미만(최소 30초)이 남았다면, 서버가 모든 원래 클레임(scopesub 포함)을 보존한 새 토큰을 생성해요.
  3. 갱신된 토큰이 Refreshed-API-Token 응답 헤더로 반환돼요.
  4. 클라이언트의 _update_auth() 훅이 이 헤더를 감지하고 BearerAuth 인스턴스를 이후 요청에 투명하게 업데이트해요.

미들웨어는 execution-스코프 토큰만 갱신해요. workload-스코프 토큰은 큐 타임아웃 창에 걸칠 수 있는 크기이며 미들웨어가 명시적으로 건너뛰어요 — 워커가 재인증할 필요 없이 executor 큐 대기 시간을 살아남도록 설계됐어요. 이렇게 하면 장기 실행 task가 API 접근을 잃지 않아요.

토큰 취소 없음 (Execution API)

Execution API 토큰은 취소 대상이 아니에요. execution-스코프 토큰은 수명이 짧고(기본 10분) JWTReissueMiddleware가 자동으로 갱신해요. workload-스코프 토큰([scheduler] task_queued_timeout을 따름)은 갱신되지 않아요 — 유효 기간 후 자연히 만료돼요. 취소는 Execution API 보안 모델의 일부가 아니에요.

기본 타이밍 (Execution API)

Setting Default
[execution_api] jwt_expiration_time 600 초 (10분)
Workload 토큰 수명 (파생) [scheduler] task_queued_timeout (기본 600초)
[execution_api] jwt_audience urn:airflow.apache.org:task
토큰 갱신 임계값 남은 유효 시간의 20% (최소 30초)

Dag File Processor와 Triggerer

Dag File ProcessorTriggerer는 Execution API와도 상호작용하는 내부 Airflow 컴포넌트지만, 네트워크 대신 인프로세스(in-process) 전송(InProcessExecutionAPI)으로 상호작용해요. 이 인프로세스 API는:

  • ASGI/WSGI 브리지를 사용해 같은 프로세스 내에서 직접 Execution API 애플리케이션을 실행해요.
  • JWT 인증을 잠재적으로 우회해요 — JWT bearer 의존성이 항상 "execution" 스코프의 합성 TIToken을 반환하도록 재정의되어, 토큰 검증을 사실상 우회해요.
  • 리소스별 접근 제어(connection, variable, XCom 접근 검사가 항상 허용되도록 재정의됨)도 잠재적으로 우회해요.

Airflow는 이러한 컴포넌트에서 DAG 작성자 코드가 우발적으로 데이터베이스에 직접 접근하는 것을 방지하는 소프트웨어 가드를 구현해요. 하지만 DAG 파일을 파싱하고 trigger 코드를 실행하는 하위 프로세스가 부모 프로세스와 같은 Unix 사용자로 실행되므로, 이러한 가드는 의도적 접근을 보호하지 못해요. 의도적으로 악의적인 DAG 작성자는 부모 프로세스의 데이터베이스 자격 증명(/proc/<PID>/environ, 구성 파일, 또는 secrets manager 접근을 통해)을 검색해 유효한 JWT 토큰 없이 메타데이터 데이터베이스와 모든 Execution API 작업에 대한 완전한 읽기/쓰기 접근을 얻을 수 있어요.

이것은 격리가 배포 수준에서 구현되는 워커/task 실행과 대조적이에요 — 민감한 데이터베이스 자격 증명 구성이 전혀 배포 구성에 설정되지 않아 Airflow 프로세스에 제공되지 않고, 오로지 Execution API를 통해서만 통신하기 때문이에요.

기본 배포에서 단일 Dag File Processor 인스턴스가 모든 팀의 DAG 파일을 파싱하고, 단일 Triggerer 인스턴스가 모든 팀의 트리거를 처리해요. 즉, 다른 팀의 DAG 작성자 코드가 같은 프로세스 내에서 실행되며, 인프로세스 Execution API와 메타데이터 데이터베이스에 공유 접근할 수 있어요.

격리가 필요한 다중 팀 배포에서 배포 관리자는 팀별로 별도의 Dag File Processor와 Triggerer 인스턴스를 배포 수준 조치로 실행해야 해요 — Airflow는 팀별 DFP 또는 Triggerer 인스턴스의 내장 지원을 제공하지 않아요. 별도 인스턴스로도 각각 부모 프로세스와 같은 Unix 사용자를 유지해요. 자격 증명 검색을 방지하려면 배포 관리자가 Unix 사용자 수준 격리(하위 프로세스를 다른 낮은 권한 사용자로 실행) 또는 네트워크 수준 제한을 구현해야 해요.

전체 보안 영향, 배포 강화 지침, 계획된 전략적·전술적 개선은 Airflow 보안 모델을 참고하세요.

워크로드 격리와 현재 한계

워크로드 격리 보호, 현재 한계, 계획된 개선에 대한 자세한 논의는 워크로드 격리와 현재 한계를 참고하세요.

구성 참조 (Configuration Reference)

모든 JWT 관련 구성 파라미터:

Parameter Default Description
[api_auth] jwt_secret 누락 시 자동 생성 토큰 서명용 대칭 비밀 키. 모든 컴포넌트에서 동일해야 함. jwt_private_key_path와 상호 배타적.
[api_auth] jwt_private_key_path None PEM 인코딩된 개인 키(RSA 또는 Ed25519) 경로. jwt_secret과 상호 배타적.
[api_auth] jwt_algorithm GUESS 서명 알고리즘. 키 유형에서 자동 감지: 대칭은 HS512, RSARS256, Ed25519EdDSA.
[api_auth] jwt_kid Auto (RFC 7638 thumbprint) 토큰 헤더에 넣는 키 ID. 대칭 키에서 무시됨.
[api_auth] jwt_issuer None 발급자 클레임 (iss). 배포별로 고유한 것이 권장됨.
[api_auth] jwt_audience None REST API 토큰의 audience 클레임 (aud).
[api_auth] jwt_expiration_time 86400 (24h) REST API 토큰 수명 (초).
[api_auth] jwt_cli_expiration_time 3600 (1h) CLI 토큰 수명 (초).
[api_auth] jwt_leeway 10 토큰 검증용 클록 스큐 허용 (초).
[api_auth] trusted_jwks_url None 토큰 검증용 JWKS 엔드포인트 URL 또는 로컬 파일 경로. jwt_secret과 상호 배타적.
[execution_api] jwt_expiration_time 600 (10 min) Execution API execution-스코프 토큰 수명 (초).
[scheduler] task_queued_timeout 600.0 (10 min) 큐 기아 타임아웃. workload-스코프 토큰 수명도 같은 값으로 설정.
[execution_api] jwt_audience urn:airflow.apache.org:task Execution API 토큰의 audience 클레임.

중요 (Important)

모든 Airflow 컴포넌트 간 시간 동기화는 매우 중요해요. NTP(예: ntpd 또는 chrony)를 사용해 클록을 동기화하세요. 구성된 jwt_leeway를 넘는 클록 스큐는 인증 실패를 일으킬 거예요.

더 알아보기 (Learn more)