Trino JWT 인증
Trino coordinator 가 Keycloak 발급 JWT 를 검증해 GenD API 요청을 인가합니다. 본 문서는 활성화 구성, Keycloak client 설정, 토큰 발급 방법, 장애 대응을 다룹니다.
개요
GenD API 는 기동 후 Keycloak 의 trino client 로 service-account 토큰을 발급받아 메모리 캐시(만료 60 초 전 재발급)에 두고, 모든 Trino DB-API / REST 요청에 Authorization: Bearer <token> 헤더로 첨부합니다. Trino coordinator 는 JWKS 엔드포인트에서 공개키를 받아 RS256 서명을 검증합니다.
활성화 구성
Trino coordinator
infra/helm/trino/values.yaml 의 server.coordinatorExtraConfig 로 coordinator 전용 JWT 속성을 주입합니다. worker 는 JWT 속성을 받으면 Configuration property 'X' was not used 로 crash 하므로 additionalConfigProperties 에 넣지 마십시오.
server:
coordinatorExtraConfig: |
http-server.authentication.type=JWT
http-server.authentication.allow-insecure-over-http=true
http-server.authentication.jwt.key-file=https://gend.genon.ai/auth/realms/gend/protocol/openid-connect/certs
http-server.authentication.jwt.principal-field=preferred_username
http-server.authentication.jwt.required-issuer=https://gend.genon.ai/auth/realms/gend
allow-insecure-over-http=true 이유: 클러스터 내부 pod-to-pod 통신은 HTTP 이고, process-forwarded=true + X-Forwarded-Proto: https 로 HTTPS 위장 시 Trino 가 응답 nextUri 를 https://trino.svc:8080 으로 rewrite 해 후속 polling 이 SSL 오류로 실패합니다. 네트워크 경계는 AKS NetworkPolicy 로 통제하므로 HTTP 위 JWT 를 허용합니다. 외부 노출 시에는 Ingress TLS 종단 또는 mTLS sidecar 를 함께 쓰십시오.
GenD API
환경변수:
GEND_TRINO_JWT_ENABLED=true
GEND_TRINO_JWT_ISSUER_URL=https://gend.genon.ai/auth/realms/gend
GEND_TRINO_JWT_CLIENT_ID=trino
GEND_TRINO_JWT_CLIENT_SECRET=<keycloak trino client secret>
GEND_TRINO_HOST=trino.gend.svc.cluster.local
GEND_TRINO_PORT=8080
현재 GEND_TRINO_JWT_CLIENT_SECRET 은 Deployment spec 에 평문 value: 로 주입되어 있습니다 (kubectl set env 로 #533 활성화 시 투입). 기존 trino-secrets SealedSecret 은 Trino 내부 통신용(shared-secret, keystore-password) 이라 JWT client_secret 전용 키가 없습니다. 프로덕션 완성도를 위해 gend-api 전용 SealedSecret 또는 trino-secrets 에 keycloak-client-secret 키 추가가 필요합니다 (별도 이슈로 추적 예정).
Keycloak 설정
trino client 는 service accounts 를 활성화한 confidential client 로 구성합니다.
| 항목 | 값 |
|---|---|
| Client ID | trino |
| Access Type | confidential |
| Service Accounts Enabled | ON |
| Direct Access Grants | OFF (프로덕션) |
| Valid Redirect URIs | 불필요 |
토큰 수동 발급 (운영/디버깅용):
# client_secret 조회 (현재는 Deployment spec 에서 — 과도기)
GEND_TRINO_JWT_CLIENT_SECRET=$(kubectl --context aks-genos-prod get deploy gend-api -n gend \
-o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="GEND_TRINO_JWT_CLIENT_SECRET")].value}')
TRINO_TOKEN=$(curl -s -X POST \
"https://gend.genon.ai/auth/realms/gend/protocol/openid-connect/token" \
-d "grant_type=client_credentials" \
-d "client_id=trino" \
-d "client_secret=${GEND_TRINO_JWT_CLIENT_SECRET}" | jq -r .access_token)
Trino API 검증:
curl -v -X POST "http://trino.gend.svc.cluster.local:8080/v1/statement" \
-H "Authorization: Bearer ${TRINO_TOKEN}" \
-H "X-Trino-User: gend-api" \
--data "SELECT 1"
# → 200 OK, query id 반환
client_secret 회전 절차
GEND_TRINO_JWT_CLIENT_SECRET 이 Deployment 평문 env 로 있는 동안은 다음처럼 처리합니다. 전용 SealedSecret 이 생기면 Keycloak 회전 → SealedSecret 재봉인 → ArgoCD 자동 반영 흐름으로 교체 예정.
- Keycloak admin 콘솔 → Clients →
trino→ Credentials 탭 → Regenerate secret - Deployment env 갱신 (현재 과도기):
kubectl --context aks-genos-prod set env deploy/gend-api -n gend \GEND_TRINO_JWT_CLIENT_SECRET='<new_secret>'kubectl --context aks-genos-prod rollout status deploy/gend-api -n gend --timeout=120s
- (목표 아키텍처) gend-api 전용 SealedSecret 으로 이전 후에는 다음으로 대체:
kubectl create secret generic gend-api-trino-jwt \--from-literal=client-secret='<new_secret>' \-n gend --dry-run=client -o yaml \| kubeseal -o yaml > infra/sealed-secrets/sealed-secrets/gend-api-trino-jwt.yaml# PR 머지 → ArgoCD 가 Secret 업데이트 → gend-api 자동 재기동
- 토큰 캐시는
expires_in(Keycloak 기본 5 분) 기준으로 만료되며 만료 60 초 전 자동 재발급됩니다. 즉시 회전이 필요하면 Pod 재기동으로 캐시 비우기.
트러블슈팅
| 증상 | 원인 | 처치 |
|---|---|---|
401 Unauthorized on /v1/statement | Bearer 미첨부 또는 만료 | GEND_TRINO_JWT_* env 확인, gend-api Pod 재시작 |
Configuration property 'http-server.authentication.type' was not used (worker crash) | JWT 속성이 additionalConfigProperties 에 들어감 | server.coordinatorExtraConfig 로 이동 |
SSLError: record layer failure polling 중 | allow-insecure-over-http=false 기본값 + client 가 HTTP 스킴 | values.yaml 에 allow-insecure-over-http=true 추가, coordinator rollout |
Token is not valid — issuer mismatch | required-issuer 와 Keycloak 실제 issuer 불일치 | Keycloak realm 의 tokenIssuer 확인 후 values.yaml 수정 |
Unable to fetch JWK | coordinator 가 key-file URL 접근 불가 | 외부 URL 접근 권한/DNS 확인, 내부 Keycloak URL 로 override |
gend-api 로그 Trino JWT is enabled but Keycloak service-account token is empty | Keycloak 다운 또는 client_secret 불일치 | Keycloak health 확인, secret 재봉인 |
로그 확인
다중 레플리카 환경에서는 레이블 셀렉터를 쓰면 모든 Pod 로그가 합쳐져 조회됩니다 (kubectl logs deploy/X 는 단일 Pod). 컨텍스트/네임스페이스 플레이스홀더를 환경별로 치환하세요.
GenD API 측
kubectl --context <CONTEXT> logs -n <NAMESPACE> -l app=gend-api -c gend-api --tail=200 \
| grep -iE "keycloak|jwt|trino"
Trino coordinator 측
kubectl --context <CONTEXT> logs -n <NAMESPACE> -l app.kubernetes.io/component=coordinator --tail=200 \
| grep -iE "Authentication|JWT|Unauthorized|signature"
외부 JDBC / 파이썬 접속 (trino.gend.genon.ai) — 폐기됨
:::danger 이 경로는 더 이상 존재하지 않습니다 (2026-08-08 기준)
gend-ingress-trino-external 은 의도적으로 제거됐습니다 — PR #2693 (이슈 #2683
"prod Trino 접근제어 전면 미적용"). allow-insecure-over-http=true 가 평문 요청을 insecure
authenticator 로 폴백시켜 X-Trino-User 헤더만으로 임의 신원 행세가 가능했고, 노출면을 좁히는
것이 그 대응의 Step 0 이었습니다.
이후 ingress-nginx 자체가 EOL 로 해체(ADR-0028)되어 호스트가 응답하지 않습니다.
고아 DNS 레코드도 정리했습니다(#3160).
지금 데이터를 조회하려면: GenD UI 의 SQL 편집기, 또는 코드스페이스에서 in-cluster
trino:8080 을 사용하세요. 외부 BI 도구 직결이 필요하면 관리자에게 문의하세요 —
재개하려면 Trino 인증 자세 전환(allow-insecure-over-http=false)이 선행돼야 합니다.
아래 내용은 이력 참조용이며 그대로 따라 해도 연결되지 않습니다. :::
아래 절차는 지금 적용하면 안 됩니다. gend-ingress-trino-external 은 prod 에서
삭제됐고, infra/ingress/trino-external.yaml 에는 적용 금지 표시가 붙어 있습니다.
제거 사유 — 이 절이 원래 "rules.json 이 catalog/table authz 를 시행한다" 고
기술했으나 그 전제가 성립한 적이 없습니다. 2026-07-28 실측 결과 prod coordinator 에
access-control.properties 자체가 없어 Trino 가 기본 정책(임퍼소네이션만 거부, 그 외
전부 허용)으로 동작했습니다. 즉 이 Ingress 는 인가가 전혀 없는 SQL 엔드포인트를
인터넷에 노출하고 있었습니다. 원인은 Helm chart 가 무시하는 values 키 + 3개월간
실패한 helm upgrade 였습니다 (docs/DESIGN_TRINO_ENGINE_ACL.md §2.5).
추가로 required-audience 가 없어 gend realm 의 임의 토큰이 통과했습니다 —
일반 사용자의 UI 로그인 토큰으로 전 테이블 조회가 가능한 구성이었습니다.
현재 상태 확인 — 외부 라우트가 실제로 닫혔는지 다음으로 검증합니다.
# Ingress 부재 확인
kubectl --context aks-genos-prod -n gend get ingress | grep -i trino # 출력 없음
# 외부 도메인에서 SQL 실행 시도 — Trino 가 아닌 다른 백엔드가 응답한다
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
-H "X-Trino-User: admin" --data "SELECT 1" \
https://trino.gend.genon.ai/v1/statement
# → 405 (nginx). Trino 라면 200 + JSON(nextUri) 이 온다.
Ingress 를 제거하면 해당 호스트가 다른 백엔드로 폴백해 GET / 에 200(HTML)을
돌려줄 수 있습니다. 상태 코드만 보고 "아직 열려 있다" 고 판단하지 마세요 —
위처럼 실제 SQL 실행을 시도해 응답이 Trino JSON 인지 확인해야 합니다.
재개 조건 — ADR-0030 의 Step 3
(파일 기반 ACL 활성화) 완료 후. 그때 required-audience 설정과 소스 IP 제한도 함께
검토합니다. 재개 전까지 외부 SQL 접근이 필요하면 SQL 에디터·API·MCP 를 쓰십시오.
:::
클러스터 밖(노트북·BI 도구·ETL)에서 Trino coordinator 에 직접 접속하는 경로입니다 (#2376). kubectl port-forward 없이 공개 TLS Ingress 로 붙습니다.
클라이언트 ──HTTPS(443, TLS)──▶ nginx ingress ──HTTP(8080)──▶ trino coordinator
(Authorization: Bearer JWT) (Let's Encrypt 종단) (JWKS 로 JWT 검증)
인증은 UI 와 동일하게 Keycloak JWT 입니다. oauth2-proxy 를 거치지 않으므로 클라이언트가 직접 Bearer 토큰을 첨부합니다 (Trino coordinator 가 issuer/서명 검증).
과거 이 문서는 "access-control.name=file 이 배포된 적 없어 인가가 시행되지
않는다"고 정정했었습니다. 2026-08-05 부로 파일 ACL 이 prod 에 활성화됐습니다
(helm rev 24): coordinator 에 access-control.name=file + file group provider
(refresh-period=60s) 라이브, 서비스 계정의 gendpg 접근 차단 실측.
rules.json 은 이제 수작업 산출물이 아닙니다 — trino-acl-sync CronJob
(매 5분)이 DataGrant/AccessPolicy 에서 자동 생성해 발행 전 불변식
(_assert_invariants — 서비스 규칙 존재·gendpg 차단)을 통과한 것만
cm/trino-access-control 에 patch 합니다 (직전본은 rules.json.bak 키 보존,
coordinator 는 60초 주기로 자동 반영). 수동 규칙 편집은 다음 동기화에
덮어써집니다 — 권한 변경은 DataGrant(API/UI)로 하십시오.
security.refresh-period 에 달려 있다 (v1.1+)coordinator 의 access-control.properties 에 security.refresh-period 가
없으면 Trino 는 rules.json 을 기동 시 한 번만 파싱합니다. 이 경우 CronJob 이
ConfigMap 을 갱신하고 TRINO_ACL_SYNC_APPLIED 성공 로그를 남기며 파드 안 파일까지
바뀌어도 권한 판정은 전혀 바뀌지 않습니다(2026-08-05 prod 실측 — 동기화가
도입 이후 엔진에 반영된 적이 없었음).
# 진단 — 이 3줄이 모두 보여야 자동 반영이 성립한다
kubectl -n gend exec deploy/trino-coordinator -- cat /etc/trino/access-control.properties
# access-control.name=file
# security.config-file=/etc/trino/access-control/rules.json
# security.refresh-period=60s ← 없으면 재기동 전까지 무반영
없다면 infra/helm/trino/values-prod.yaml 의
coordinator.additionalConfigFiles.access-control.properties 에 추가하고
helm upgrade → coordinator 재기동. 회귀 가드는
scripts/test_cross_pr_consistency.py::test_acl_sync_requires_trino_refresh_period
(acl-sync CronJob 이 있으면 이 값이 필수).
규칙 모양 불변식 — 생성기·검증기가 함께 고정합니다(어긋나면 패리티 테스트 실패):
| 대상 | 요구 |
|---|---|
서비스 계정(gend-api·gend-batch·dagster 등) | catalogs/schemas/tables 세 섹션 모두에 규칙 존재 (한 섹션만 빠져도 그 단계에서 전면 거부) |
쓰기 카탈로그(iceberg·hive·nessie) | 서비스 계정 첫 매칭 규칙에 OWNERSHIP·GRANT_SELECT 포함 — 없으면 CREATE TABLE/VIEW 가 거부돼 데이터마트 생성·파이프라인 첫 적재가 막힙니다(v1.1+ 에서 시행) |
gendpg | 서비스 계정 규칙의 catalog 정규식이 gendpg 에 매칭되면 발행 거부 |
★ 파일 ACL 은 첫 매칭 규칙만 적용합니다 — 뒤에 넉넉한 규칙이 있어도 앞의 좁은 규칙이 이깁니다. 규칙을 손으로 추가할 때 순서를 반드시 확인하십시오.
수동 즉시 반영·상태 확인:
kubectl -n gend create job --from=cronjob/trino-acl-sync acl-sync-manual
kubectl -n gend logs job/acl-sync-manual # TRINO_ACL_SYNC_APPLIED 또는 "변경 없음"
배포 전 정적 게이트: python scripts/validate_trino_rules.py <rules.json|cm.yaml>
— 스테일/수작업 규칙(서비스 규칙 부재)을 배포 전에 잡습니다 (2026-08-04 사고의
재발 방지 불변식 포함).
:::
선행 조건
DNS:— 삭제됨(#3160). 구 ingress LBtrino.gend.genon.aiA레코드20.214.90.38은 nginx 해체로 소멸.- Ingress 배포:
infra/ingress/trino-external.yaml를 대상 클러스터에kubectl apply. - coordinator
process-forwarded=true:infra/helm/trino/values.yaml반영 후helm upgrade+ 재시작 (결과 폴링nextUri가https://trino.gend.genon.ai/...로 생성되도록). - (선택) gend-api self-service JDBC 가이드용
GEND_TRINO_EXTERNAL_HOST=trino.gend.genon.ai설정 (#1157).
외부에 열리므로 인증/인가가 유일한 방어선입니다. TLS 로 토큰은 암호화되지만, required-audience 가 아직 미설정(#530)이라 gend realm 의 임의 토큰이 통과합니다. BI/ETL 은 gend-api 의 trino service-account(client_secret)를 재사용하지 말고 전용 Keycloak client 로 발급하고, rules.json 으로 catalog/table 권한을 최소화하세요.
- 세션 user = 토큰 principal:
user(DB-API/JDBC)는 토큰preferred_username(client_credentials 면service-account-<client_id>)과 반드시 일치해야 합니다. 다른 user 로 붙으면 impersonation 규칙이 없어Access Denied: User ... cannot impersonate user ...로 거부됩니다 (dev 실증 #2376). 임의 user 실행이 필요하면rules.json에 impersonation 규칙을 추가하세요. - catalog 권한: 연결 principal 이
rules.json의 group(admin/analyst/…)에 매핑돼야 실제 데이터(iceberg/tpch 등) 쿼리가 됩니다. 매핑이 없으면SELECT 1같은 catalog 없는 쿼리만 통과합니다.
파이썬 (trino DB-API)
import os, requests
from trino.auth import JWTAuthentication
from trino.dbapi import connect
# 1) Keycloak 에서 JWT 발급 (프로그램적 접속 — client_credentials)
token = requests.post(
"https://gend.genon.ai/auth/realms/gend/protocol/openid-connect/token",
data={"grant_type": "client_credentials",
"client_id": os.environ["TRINO_CLIENT_ID"], # 전용 client 권장
"client_secret": os.environ["TRINO_CLIENT_SECRET"]},
timeout=15,
).json()["access_token"]
# 2) 외부 엔드포인트로 HTTPS 접속 (port-forward 불필요)
conn = connect(
host="trino.gend.genon.ai", port=443, http_scheme="https",
auth=JWTAuthentication(token),
# user 는 토큰 principal(preferred_username)과 반드시 동일해야 함.
# client_credentials 면 "service-account-<client_id>". 다르면 "cannot impersonate" 거부.
user="service-account-trino", catalog="iceberg", schema="default",
)
cur = conn.cursor()
cur.execute("SELECT nationkey, name FROM tpch.sf1.nation LIMIT 5")
print(cur.fetchall())
JDBC (DBeaver / Trino JDBC 드라이버)
jdbc:trino://trino.gend.genon.ai:443?SSL=true&accessToken=<JWT>&user=service-account-trino
SSL=true 는 필수(443 TLS), accessToken 은 위 1)에서 발급한 JWT, user 는 토큰 principal 과 동일해야 합니다(아래 주의 박스 참고). DBeaver 는 Driver properties 에 accessToken 을 넣습니다.
변경 이력
- Phase 5 활성화: #533 (2026-04-24)
- 운영 가이드 문서 추가: #540
- upstream 문서 개선: trinodb/charts#421
- 외부 JDBC TLS Ingress (trino.gend.genon.ai): #2376