현재 격리의 한계 - DB 자격증명 탈취와 JWT 우회 리스크
Apache Airflow Official Documentation (in-repo snapshot) — Apache Software Foundation security/security_model.rst - "Current isolation limitations" 섹션 (약 433-544행)
이 모듈을 다 읽으면
- 소프트웨어 가드가 왜 우발적 접근만 막고 의도적 접근은 막지 못하는지 그 메커니즘(동일 Unix 사용자, /proc 접근)을 설명할 수 있다
- Execution API가 실제로 강화한 부분(시크릿 백엔드 deny 존중, 역직렬화 allowlist, typed JWT claims)과 여전히 남은 한계(공유 자원)를 구분할 수 있다
Airflow 3는 워커의 직접 DB 접근을 차단해 격리를 크게 개선했지만, Dag Author 간의 완벽한 격리는 아직 달성되지 않았다. Dag File Processor와 Triggerer는 부모 프로세스와 동일한 Unix 사용자로 실행되기 때문에 소프트웨어 가드를 우회한 의도적 자격증명 탈취가 가능하고, Execution API는 몇 가지 실질적 보강(시크릿 백엔드 deny 존중, 역직렬화 allowlist, typed JWT claims)을 갖췄지만 팀/태스크 간 공유 자원에 대한 근본적 격리는 아직 없다.
소프트웨어 가드 vs 의도적 접근
Airflow는 Dag Author 코드로부터의 '우발적이고 의도치 않은' 직접 DB 접근을 막는 소프트웨어 레벨 가드를 구현한다. Dag File Processor는 Dag 파일을 파싱하는 자식 프로세스를 fork하기 전에 DB 세션/연결 정보를 제거하며, 워커 태스크는 오직 Execution API만 사용한다.
그러나 이 소프트웨어 가드는 '의도적이고 악의적인' 접근까지는 막지 못한다. Dag 파일을 파싱하고 트리거 코드를 실행하는 자식 프로세스는 각각의 부모 프로세스(Dag File Processor 매니저, Triggerer)와 동일한 Unix 사용자로 실행된다. POSIX 프로세스 격리 방식 때문에 동일 사용자로 실행되는 자식 프로세스는 여러 경로로 부모의 자격증명을 탈취할 수 있다: 리눅스에서는 기본적으로 동일 사용자의 다른 프로세스가 ``/proc/<PID>/environ``을 읽을 수 있어 환경 변수로 전달된 DB 접속 문자열(``AIRFLOW__DATABASE__SQL_ALCHEMY_CONN`` 등)을 읽어낼 수 있고(태스크 supervisor에 구현된 dumpable 속성 설정으로 방지 가능), 설정 파일로 저장된 값도 동일 사용자가 읽을 수 있는 파일이라면 자식이 읽을 수 있으며, ``_CMD`` 접미사 옵션은 자식이 동일 명령을 실행해 시크릿을 가져올 수 있고, 부모가 시크릿 매니저를 쓴다면 자식도 프로세스 환경/파일시스템에서 얻은 자격증명으로 같은 매니저에 접근할 수 있다.
이는 의도적으로 악의적인 Dag Author가 DB 자격증명을 탈취해 메타데이터 DB에 대한 완전한 읽기/쓰기 접근권 - Dag, 태스크 인스턴스, Connection, Variable을 모두 수정할 수 있는 권한 - 을 얻을 수 있음을 의미한다. 소프트웨어 가드는 실수로 인한 접근(예: Airflow 2 습관으로 ``airflow.settings.Session``을 import하는 경우)을 막을 뿐, 결연한 공격자를 막지는 못한다.
워커에서는 Deployment Manager가 워커 프로세스에 DB 자격증명을 아예 주지 않도록 구성하면 격리가 더 강해질 수 있다 - 워커는 짧은 수명의 JWT 토큰으로 Execution API만 사용하도록 하는 것이 원칙이며, 접근 가능한 자격증명이 전혀 없도록 구성된 경우 워커 태스크는 진짜로 메타데이터 DB에 직접 접근할 수 없다.
핵심 포인트
- 소프트웨어 가드는 우발적 DB 접근만 막고, 동일 Unix 사용자로 실행되는 의도적 자격증명 탈취는 막지 못한다
- /proc/<PID>/environ, 설정 파일, _CMD 시크릿, 시크릿 매니저 접근 등이 부모 자격증명 탈취 경로가 될 수 있다
- 악의적 Dag Author는 DB 자격증명을 탈취해 Dag/태스크 인스턴스/Connection/Variable에 대한 완전한 읽기/쓰기 권한을 얻을 수 있다
- 워커에 DB 자격증명 자체를 주지 않도록 구성하면 워커 레벨의 격리는 실질적으로 강화될 수 있다
DFP/Triggerer의 JWT 우회와 팀 간 공유
Dag File Processor와 Triggerer는 사용자 코드를 실행하는 프로세스이면서, Execution API 접근에 JWT 인증을 우회하는 in-process 전송을 사용한다. 이들이 실행하는 코드가 Dag 파일과 트리거 코드이므로, 이 컴포넌트에서 실행되는 Dag Author 코드는 소프트 보호를 우회할 경우 유효한 JWT 토큰 없이도 모든 Execution API 작업 - 어떤 Connection, Variable, XCom이든 읽는 것을 포함 - 에 무제한 접근할 수 있다.
더구나 Dag File Processor는 직렬화된 Dag를 저장하기 위해 메타데이터 DB에 직접 접근한다. 앞서 설명한 대로 Dag File Processor 컨텍스트에서 실행되는 Dag Author 코드는 부모 프로세스로부터 DB 자격증명을 탈취해 DB에 직접 접근할 수 있고, 프로세스 환경에 있다면 JWT 서명 키 설정까지 탈취할 수 있다. Dag Author가 JWT 서명 키를 얻으면 임의의 토큰을 위조할 수 있다.
기본 배포에서는 단일 Dag File Processor 인스턴스가 모든 Dag 파일을 파싱하고 단일 Triggerer 인스턴스가 모든 트리거를 처리한다 - 팀 배정과 무관하다. 팀별 Dag File Processor/Triggerer 인스턴스에 대한 내장 지원은 없다. 이는 서로 다른 팀의 Dag Author 코드가 동일한 프로세스 내에서 실행되며, in-process Execution API와 직접 DB 접근을 잠재적으로 공유함을 의미한다.
분리가 필요한 멀티팀 배포에서는 Deployment Manager가 팀별로 별도의 Dag File Processor/Triggerer 인스턴스를 배포 레벨 조치로 운영해야 한다(예: 각 인스턴스가 특정 팀에 속한 번들만 처리하도록 구성). 그러나 별도 인스턴스를 두더라도 각 Dag File Processor/Triggerer는 여전히 메타데이터 DB에 직접 접근할 수 있어, 그 컴포넌트에서 실행되는 Dag Author 코드는 부모로부터 자격증명을 탈취해 다른 팀에 속한 데이터를 포함해 DB에 직접 접근할 수 있다 - Deployment Manager가 Unix 사용자 레벨 격리를 구현하지 않는 한 그렇다.
핵심 포인트
- DFP/Triggerer는 in-process 전송으로 JWT 인증을 우회하며, 소프트 보호가 뚫리면 임의 Connection/Variable/XCom에 무제한 접근할 수 있다
- Dag File Processor는 직렬화된 Dag 저장을 위해 DB에 직접 접근하며, JWT 서명 키까지 프로세스 환경에서 탈취되면 임의 토큰 위조가 가능해진다
- 기본 배포는 단일 DFP/단일 Triggerer가 모든 팀을 처리한다 - 팀별 인스턴스 내장 지원은 없다
- 팀별로 인스턴스를 분리해도 각 인스턴스가 여전히 직접 DB 접근을 가지므로, Unix 사용자 레벨 격리 없이는 다른 팀 데이터 접근 가능성이 남는다
Execution API의 공유 자원과 실질적 보강
모든 워커 워크로드는 동일한 서명 키로 서명되고 동일한 audience를 공유하는 토큰으로 같은 Execution API에 인증한다. ``ti:self`` 스코프 강제는 한 워커가 다른 태스크의 특정 엔드포인트(heartbeat, 상태 전이)에 접근하는 것은 막지만, Connection·Variable·XCom 같은 공유 자원은 모든 태스크에서 접근 가능하다. 서로 다른 팀이나 Dag Author에 속한 태스크 간의 격리는 Execution API 레벨에서 없다.
다만 ``ti:self``를 넘어 Execution API가 실제로 강제하는 것들이 있다. 첫째, 시크릿 백엔드의 거부(deny)가 존중된다 - Execution API가 Connection/Variable 조회에 대해 401/403을 반환하면 SDK의 ``ExecutionAPISecretsBackend``는 ``None``을 반환하는 대신 ``PermissionError``의 서브클래스인 ``AirflowSecretsBackendAccessDenied``를 raise한다. 시크릿 백엔드 디스패처는 권위 있는 백엔드가 명시적으로 요청을 거부했을 때 (인가 검사를 전혀 하지 않는 ``EnvironmentVariablesBackend`` 같은) 덜 제한적인 백엔드로 넘어가서는(fall through) 안 된다. ``NOT_FOUND`` 응답은 기존의 fall-through 동작을 유지해 '여기서는 못 찾음' 경로는 그대로 쓸 수 있다. 이는 인가 컨트롤은 작동했지만 그 거부가 miss로 취급되던 'Type C' 갭을 막는다.
둘째, 역직렬화 allowlist가 강화되었다 - 직렬화기의 클래스명 allowlist 정규식이 전체 문자열 일치(full-string match)로 앵커링되어, 공격자가 통제하는 값이 허용된 접미사를 붙여 허용되지 않은 클래스명을 위장해 밀반입할 수 없다.
셋째, typed JWT claims 스키마가 도입되었다 - 암호학적 검증을 통과한 이후에도 claim은 ``scope`` 리터럴을 강제하는 typed Pydantic 스키마(``TIClaims``)를 거치고, 이어서 ``sub``를 UUID로 파싱하는 ``TIToken``을 거친다. ``scope``가 알 수 없는 값이거나 ``sub``가 유효한 UUID가 아닌 토큰은 어떤 라우트 핸들러도 실행되기 전에 거부된다.
핵심 포인트
- 모든 워커는 동일 서명 키/audience의 토큰을 쓰며, ti:self는 특정 엔드포인트만 제한하고 Connection/Variable/XCom 같은 공유 자원은 격리하지 않는다
- 시크릿 백엔드가 401/403으로 거부하면 SDK는 AirflowSecretsBackendAccessDenied를 raise하며 덜 제한적인 백엔드로 fall through하지 않는다(NOT_FOUND는 예외)
- 역직렬화 클래스명 allowlist가 full-string match로 앵커링되어 접미사 위장 공격을 막는다
- TIClaims/TIToken으로 scope 리터럴과 sub의 UUID 형식을 검증해, 형식이 이상한 토큰은 라우트 핸들러 실행 전에 거부된다
서명 키 공유와 로그를 통한 민감값 유출
대칭 키 모드(``[api_auth] jwt_secret``)에서는 토큰 생성과 검증에 동일한 시크릿 키가 사용된다. 이 시크릿에 접근할 수 있는 어떤 컴포넌트든 다른 태스크 인스턴스에 대한 토큰이나 상위 스코프를 가진 토큰을 포함해 임의의 claim으로 토큰을 위조할 수 있다. 다만 이 시크릿이 배포 설정을 통해 오직 API Server와 Scheduler에만 제공된다면 시스템 보안에 영향을 주지 않는다.
Dag Author는 환경 변수나 설정 값을 태스크 로그에 출력하는 코드(예: ``print(os.environ)``)를 작성할 수 있다. Airflow는 로그에서 알려진 민감 값을 마스킹하지만, 마스킹은 값 패턴을 인식하는 데 의존한다. 의도적이든 실수든 원본 환경 변수를 그대로 로그에 남기는 Dag Author는 DB 자격증명, JWT 서명 키, Fernet 키 등 시크릿을 태스크 로그에 노출시킬 수 있다. Deployment Manager는 태스크 로그 접근을 제한하고, 민감한 설정이 필요한 컴포넌트에만 제공되도록 해야 한다.
핵심 포인트
- 대칭 키(jwt_secret) 모드는 생성/검증에 동일 키를 쓰므로, 그 키에 접근 가능한 컴포넌트는 임의 토큰을 위조할 수 있다
- 이 시크릿이 API Server/Scheduler에만 제공되도록 제한되어 있다면 시스템 보안에는 영향이 없다
- print(os.environ) 같은 코드는 마스킹 패턴을 우회해 DB 자격증명·JWT 서명 키·Fernet 키를 태스크 로그에 노출시킬 수 있다