ADR-0030: Trino 엔진 계층 접근제어 — 파일 기반 ACL + 신원 전파
- 상태: 승인
- 날짜: 2026-07-29
- 관련: #2683 (본 결정), #2673 (admin 마스킹 우회 제거), #2686 (마스킹 적용 범위), #2654 (코드스페이스 보안 개선)
- 상세 설계:
docs/DESIGN_TRINO_ENGINE_ACL.md
배경
GenD 의 PII 마스킹·행 필터·DataGrant 는 Python 시행기(query_policy_enforcer.py)에서만
동작하고, Trino 엔진 자체에는 접근제어가 적용되어 있지 않다. 엔진에 도달하는 모든 경로가
통제를 우회한다.
이것을 "API 계층만 강화" 로 끝낼 수 없는 이유는 규제 요건이 통제의 존재가 아니라 우회 경로의 부재를 판정 기준으로 삼기 때문이다.
- ISMS-P 2.6.4 결함 사례: "DB 접근제어 솔루션을 도입하여 운영하고 있으나 … 접근제어 솔루션을 우회하여 데이터베이스에 접속하고 있는 경우" — 세분화 수준보다 경로 커버리지가 먼저다.
- 전자금융감독규정 제27조의5: 중요원장 직접 접근 시 "작업자" 단위 기록·5년 보존.
전 쿼리가 단일
gend-api계정으로 도달하는 현 구조는 구조적으로 충족 불가다.
업계 동향도 같은 방향이다. Databricks Unity Catalog 는 행 필터·컬럼 마스크를 SQL UDF 로 쿼리 플랜에 주입하고, 플래너를 건너뛰는 credential vending 경로에서는 해당 테이블을 아예 거부한다. Snowflake 는 정책 대상 객체를 런타임에 dynamic secure view 로 재작성하고, 외부 엔진이 조회하면 쿼리를 자사 컴퓨트로 되돌려 시행한다. 두 벤더 모두 "외부 엔진을 신뢰하고 원본을 넘긴다" 를 선택지로 두지 않는다.
동시에 JDBC/ODBC 를 막는 것도 선택지가 아니다. Palantir Foundry(폐쇄망 납품)조차 read-only JDBC/ODBC 드라이버를 제공하고, Cube·dbt Semantic Layer 같은 API-first 제품들도 BI 수요 때문에 SQL 인터페이스를 추가했다. 거버넌스 전문 벤더 Immuta 는 「SQL: The Forgotten API」에서 오히려 API-only 에 반대한다. 프레이밍은 "JDBC vs 거버넌스" 가 아니라 "거버넌스된 JDBC 엔드포인트" 여야 한다.
결정
Trino 파일 기반 접근제어(access-control.name=file)를 활성화하고, 최종 사용자 신원을
Trino 로 전파한다. Ranger·OPA·Starburst·자체 Java 플러그인은 채택하지 않는다.
근거
1. 파일 기반 ACL 은 행 필터·컬럼 마스크를 네이티브 지원한다.
Trino 352 이후 rules.json 의 테이블 규칙이 filter·filter_environment·mask·
mask_environment 를 지원한다. "세분화된 통제 = 외부 정책 엔진 필요" 라는 전제가 성립하지 않는다.
2. prod Trino 는 435 이므로 대안들이 물리적으로 불가능하다.
SELECT version() 실측 435. Ranger 플러그인은 466+, OPA 는 438+ 를 요구한다.
(팟 라벨의 app.kubernetes.io/version=480 은 차트 appVersion 기본값이므로 오독 주의.)
3. 신규 상태 저장 컴포넌트가 0개다. Ranger 는 Admin 서버 + 전용 DB + 감사 저장소(Solr/ES/S3)를 prod 의존성으로 추가한다. Ranger 가 값을 하는 상황은 Hive+Spark+HDFS+Trino 에 하나의 정책을 강제할 때이고 GenD 는 그 상황이 아니다. Starburst 는 오픈소스 Trino 를 상용 배포판으로 교체하는 결정이라 접근제어 문제보다 훨씬 큰 사안이다.
4. 필요한 코드의 절반이 이미 있다.
services/trino_rules_generator.py 가 DataGrant + AccessPolicy 로부터 행 필터·컬럼 마스크를
포함한 rules 구조를 이미 생성한다. 유일한 호출부인 POST /data-grants/sync-rules 가 결과를
JSON 으로 반환만 하고 어디에도 전달하지 않을 뿐이다("In production, this would...").
5. 자체 Java 플러그인(infra/trino/gend-access-control/)은 삭제한다.
어느 환경에도 배포된 적이 없고(dependabot 항목 2개만 참조), Trino 가 @Deprecated 로 표시한
단일 컬럼 getColumnMask 를 구현하고 있어 업그레이드 시 잠재 파손 요인이다. 파일 ACL 이
같은 일을 한다.
순서가 곧 안전장치다
| 단계 | 내용 |
|---|---|
| -1 | 배포 경로 복구 — 이것 없이는 무엇을 고쳐도 prod 에 닿지 않는다 |
| 0 | 노출 축소 — 외부 Ingress·NodePort 제거, NetworkPolicy |
| 1 | 신원 전파 — 최종 사용자 identity 를 Trino 로 |
| 2 | 규칙 파이프라인 — DB → rules.json |
| 3 | ACL 활성화 |
신원 전파 없이 규칙을 켜면 전면 장애이고, 규칙 없이 노출만 줄이면 통제 공백이 남는다.
이 결정이 전제하는 실측 사실
배포 파이프라인이 고장나 있었다 (Step -1 의 근거)
"리포에는 설정이 있는데 prod 에 없다" 의 원인은 드리프트가 아니라 독립된 결함 3건이었다.
- 키 경로 오류 — 차트 정본은
coordinator.additionalConfigFiles인데 리포는 최상위에 두었다. Helm 은 스키마에 없는 최상위 키를 경고 없이 버린다 (helm template | grep -c access-control.properties= 0). - helm upgrade 가 2026-04-24 이후 전부 실패 — 손으로 만든
trino-worker-hpa가.spec.replicas를 소유해 Helm server-side apply 와 필드 소유권이 충돌한다. 배포 스크립트의|| echo가 이를 삼켜 3개월간 감지되지 않았다. - group provider 부재 — 규칙이 전부
group키인데 provider 가 없어 매칭 0건 → Trino 기본 "no rule matches = deny" → 전면 장애.
인증이 경로에 따라 갈린다 (Step 0/1 의 근거)
allow-insecure-over-http=true 는 "HTTP 위 JWT 허용" 이 아니라 평문 HTTP 요청을
insecure authenticator 로 폴백시켜 X-Trino-User 헤더를 그대로 신뢰하게 만든다.
process-forwarded=true 와 합쳐져 다음과 같이 갈린다:
| 경로 | 인증 |
|---|---|
외부 Ingress (X-Forwarded-Proto: https) | JWT 강제 (단 required-audience 미설정) |
| in-cluster 평문 HTTP | 없음 — 임의 신원 자기주장 |
| 코드스페이스 | NetworkPolicy egress 가 사설 대역을 막아 도달 불가 |
부작용으로, 지금은 principal 이 없어 impersonation 거부에 걸리지 않는다 —
X-Trino-User 를 최종 사용자로 바꾸면 current_user 가 즉시 반영된다(실증).
따라서 Step 1 은 인가 동작을 바꾸지 않고 독립적으로 안전하게 배포할 수 있다.
활성화의 최대 위험은 "조용한 파손" 이다 (Step 3 의 근거)
Trino 435 는 규칙을 지연 로드하므로 rules.json 파싱에 실패해도 coordinator 는 정상
기동하고 probe 도 green 인 채 첫 인가 검사부터 전 쿼리가 실패한다. 실패를 캐시하지도,
직전 값으로 폴백하지도 않는다. ObjectMapper 의 FAIL_ON_UNKNOWN_PROPERTIES 가 켜져 있어
오타 키 하나가 전면 장애다(리포의 infra/trino/rules.json 은 실제로 _note 키를 갖고 있다).
섹션 부재 시 동작도 균일하지 않다 — impersonation 부재 = 거부,
system_information 부재 = 전면 거부, functions 부재 = system.builtin 만 허용.
현행 3섹션 ConfigMap 을 그대로 켜면 gend-api·Dagster 가 즉사한다.
결과
얻는 것
- 엔진에 도달하는 모든 경로(JDBC·BI·MCP·SDK)에 행 필터·컬럼 마스크가 적용된다 — #2686 이 별도 배선 없이 해소된다.
- 마스킹이 플래너에서 적용되므로
WHERE·JOIN·ORDER BY절을 통한 오라클 공격이 막힌다. 현재 Python 시행기는 projection 만 다뤄 이 구멍이 있다. - 개인 단위 쿼리 기록이 남아 전자금융감독규정 제27조의5 대응이 가능해진다.
- "거버넌스된 JDBC 엔드포인트" 라는 세일즈 포지션이 성립한다.
치르는 비용
- rules.json 생성·전달 파이프라인(CronJob + 전용 SA)이 새 운영 대상이 된다.
- 정책 반영에 최대 2~3분 지연이 생긴다(kubelet 전파 +
refresh-period). - Trino 435 에 고정된다 — 업그레이드 시 파일 ACL 스키마 호환성 재확인 필요.
남는 것
- Iceberg 파일 직접 접근(S3/SeaweedFS)은 이 결정의 범위 밖이다. 스토리지 자격증명을 외부에 넘기는 경로가 생기면 행·열 정책이 그 시점에 사라진다 — Snowflake Open Catalog 가 자사 문서에서 경고하는 것과 같은 구조다. 별도 평가가 필요하다.
- Python 시행기는 제거하지 않는다.
_audit_unmask()의pii_unmask감사 레코드가 사라지면 #2673 컴플라이언스 회귀이므로, 엔진 활성화 후enforce → audit로 강등만 한다.
기각한 대안
| 대안 | 기각 사유 |
|---|---|
| API 계층 단독 강화 + 엔진 직결 차단 | 정규식 SQL 재작성기는 JOIN·별칭에서 포기하고(UnsafeSqlForFilterError) workspace_id IS NULL 에 fail-open 이다. JDBC 차단은 딜을 잃으면서 감사 문제는 해결하지 못하고, 분석가가 데이터를 export 해 통제 밖으로 내보내는 더 나쁜 결과를 부른다 |
| Apache Ranger | Trino 466+ 필요(현재 435). Admin 서버 + DB + 감사 저장소가 새 stateful 의존성. 다중 엔진 정책 통합이 필요할 때의 도구인데 GenD 는 그 상황이 아니다 |
| OPA | Trino 438+ 필요. Rego + OPA 서비스 운영 부담. 신원 전파 문제를 해결해주지 않는다 — 지금 문제를 푸는 도구가 아니다. Trino 외 서비스와 정책을 공유해야 할 때 재검토 |
| Starburst Enterprise BIAC | 상용 배포판으로 엔진 교체. 파일 ACL 로 얻을 수 있는 것을 위해 치르기에 과도 |
| Secure view (SECURITY DEFINER) | current_user 가 항상 gend-api 라 전원 동일 뷰로 붕괴한다. 정책×테이블 수만큼 객체가 늘고, 사용자가 base table 에 닿을 수 있으면 무의미 |
| 자체 Java 플러그인 배선 | 배포된 적 없는 죽은 코드이고 Deprecated API 를 구현한다. 파일 ACL 이 같은 기능을 제공하므로 유지 비용만 남는다 |