Trino 에 접근제어 붙이기 : Apache Ranger 는 무엇이고 어떻게 동작하는가
Trino 에 Apache Ranger 를 붙여 인가 계층을 구성한 기록. Ranger 의 구조와 정책 평가 방식, LDAP UserSync, 그리고 Google Workspace Secure LDAP 조합에서 만난 문제들.
들어가며
작년 말부터 전사 쿼리 엔진을 Athena 에서 Trino 로 옮기는 작업을 진행하게 되었는데, 스캔량이 많아질수록 Athena 대비 비용 이점이 커지는 구조라 사용량이 늘어날수록 효과가 컸고 지금은 사내 분석 쿼리 대부분이 Trino 위에서 돌고있다.
그런데 쿼리 엔진을 우리 손으로 운영한다는것은 관리형 서비스가 대신 해주던 것들까지 같이 떠안는다는 뜻이기도 한데, 그중 하나가 접근제어였다. Athena 를 쓸 때는 IAM 이 알아서 해주던 일인데 Trino 로 넘어오면서 "누가 어떤 데이터를 조회 할 수 있는가" 를 직접 설계해야 하는 상황이 되었다.
사실 무슨 특별한 사건이 있어서 시작한 일은 아니다. 쿼리 엔진을 직접 운영하기로 한 이상 인증과 인가는 당연히 갖춰야 하는 축이었고, 데이터 엔지니어 쪽에서도 테이블이나 컬럼 단위로 접근을 나눠야 한다는 이야기가 나오던 참이었는데.
인증이야 Trino 가 자체적으로 여러 방식을 제공하니 고르면 되는 문제였고 남는 것은 인가였는데, Trino 에서 인가를 붙이는 현실적인 선택지가 사실상 Ranger 아니면 OPA 둘 중 하나라 그 둘을 놓고 검토를 시작하게 되었다.
Trino 인증 타입 : https://trino.io/docs/current/security/authentication-types.html
Trino 접근 제어
Trino 는 상태를 거의 남기지 않는다
접근제어 방식을 고르기 전에 Trino 라는 엔진의 성격을 먼저 짚어야 하는데, 여기서 갈리는것이 꽤 많은편이다.
Trino 는 자기 상태를 저장할 별도의 데이터베이스를 두지 않는다. MySQL 이나 PostgreSQL 처럼 유저 테이블이 있고 권한 테이블이 있는 구조가 아니라서, 좀 과장해서 말하면 거의 stateless 에 가깝게 동작한다고 볼 수 있다.
- 쿼리 기록 은 코디네이터의 메모리에만 들고 있다. 웹 UI 에서 최근 쿼리를 볼 수 있지만 그건 메모리에 남아있는 동안 뿐이고, 코디네이터가 재시작되면 그냥 사라진다.
- 인증에 쓰는 정보 도 마찬가지인데, 어딘가에 유저를 저장해두는 것이 아니라 파일이나 외부 시스템에서 그때그때 읽어들여 메모리에 올리는 형태다.
그래서 Trino 를 운영하려면 "남겨야 하는 것" 들을 바깥으로 빼내는 작업이 따라붙게 되는데, 우리는 쿼리 기록을 event listener 로 Kafka 에 흘려보내고 그걸 다시 테이블로 적재해서 쓰고있다.
event-listener.name=kafka
kafka-event-listener.created-event.topic=trino_queryEvent_created
kafka-event-listener.completed-event.topic=trino_queryEvent_completedTrino helm chart 내 eventListenerProperties 의 리스트 값 예시
이렇게 해두면 누가 언제 어떤 쿼리를 돌렸고 얼마나 스캔했는지를 나중에 SQL 로 조회 할 수 있는데, 반대로 이 설정을 안 해두면 코디네이터가 재시작되는 순간 해당 기록은 그냥 없어진다.
다만 이벤트를 그대로 다 흘려보내지는 않는데. Trino 가 내보내는 완료 이벤트에는 쿼리 텍스트나 스캔량처럼 우리가 실제로 들여다보는 것들 말고도 실행 계획 전문이나 오퍼레이터별 통계, 태스크 통계 같은것이 함께 들어있어서, 쿼리가 무거워질수록 이벤트도 같이 커지게 된다. 그래서 조회에 쓰지 않는 무거운 필드는 아래와 같이 아예 제외하고 보내고 있다.
kafka-event-listener.excluded-fields=jsonPlan,payload,operatorSummaries,taskStatistics,plan,planNodeStatsAndCosts,outputBufferMetrics,optimizerRulesSummaries,routines,catalogMetadataMetricsTrino helm chart 내 eventListenerProperties 의 리스트 값 예시
전부 남겨두면 언젠가 쓸 데가 있을것 같기도 한데, 실제로는 Kafka 와 적재 테이블 용량만 늘고 조회도 같이 느려지기 때문에, 감사 목적으로 필요한 필드만 골라 적재하고 실행 계획 같은것은 필요할 때 EXPLAIN 으로 다시 뽑는 편이 낫다고 보고있다.
권한도 같은 맥락인데, Trino 안에 권한을 저장할 곳이 없으니 접근제어 역시 외부에 두고 참조하는 형태가 될 수밖에 없다.
Trino 는 인증까지만 자기가 하고, 인가는 넘긴다
인증(authn)과 인가(authz)가 어떻게 다른지를 새삼 설명할 필요는 없을것 같고, 대신 Trino 안에서 이 둘의 담당자가 어디서 갈리는지는 짚어두는것이 뒤의 이야기에 필요하다.
- 인증 은 Trino 가 자체적으로 처리하는데, 방식을 설정으로 고르면 되고 결과물은 principal 하나가 Trino user 로 확정되는것까지다.
- 인가 는 Trino 가 스스로 판단하지 않고
system access control이라는 확장 지점으로 넘기는데, 여기에 무엇을 끼우느냐에 따라 판정 주체가 통째로 바뀐다. 처음부터 갈아끼우도록 설계되어있는 셈이다.
Trino 는 인증 방식을 여러 가지 제공한다. 우리가 실제로 쓰는 것은 둘인데.
authenticationType: "OAUTH2,PASSWORD"Trino helm chart 내 값 예시
OAuth2 는 사람이 쓰는 경로다. 브라우저나 클라이언트에서 회사 계정으로 로그인하면 그 principal 이 그대로 Trino user 가 된다.
Password 쪽은 사람이 아닌 것들이 쓰는 경로인데, Redash 나 Airflow, 각종 배치 잡처럼 브라우저 로그인을 할 수 없는 서비스 계정에는 username/password 를 발급해서 쓰고있다.
이 둘 말고도 Trino 공식 문서를 보면 LDAP 직접 인증이나 Kerberos, 클라이언트 인증서, JWT, Salesforce 등 선택지가 꽤 많으니 환경에 맞는것을 고르면 된다.
그리고 여기서 인증은 끝이다. Trino 는 "이 사람이 누구인지" 까지만 알려주고, "그래서 무엇을 할 수 있는지" 는 접근제어 시스템 쪽으로 넘겨버린다. Ranger 가 담당하는 부분이 정확히 이 뒷단이다.
왜 Ranger 였나
Trino 가 제공하는 인가 방식은 크게 셋인데, JSON 파일 기반과 Apache Ranger 기반, 그리고 OPA(Open Policy Agent) 기반이다.
| 방식 | 정책을 어디에 쓰나 | 판단 |
|---|---|---|
| file | JSON 파일 (k8s manifest) | 권한 하나 바꾸는 데 매니페스트 수정과 배포가 필요 |
| OPA | REGO 로 작성한 policy | DSL 을 새로 배워야 하고, 그래도 결국 매니페스트를 거친다 |
| Ranger | Ranger 자체 DB (웹 UI) | 컴포넌트를 따로 운영해야 함 |
앞의 두 방식을 제친 이유는 사실 하나로 수렴하는데, 권한을 바꾸려면 매번 DevOps 엔지니어가 개입해야 한다는 점이다.
"이 테이블 권한 좀 주세요" 라는 요청이 올 때마다 매니페스트를 고치고 PR 을 올리고 배포를 기다려야 한다면, 권한 관리라는 일이 통째로 인프라 팀의 큐에 쌓이게 된다. 지금 누가 무슨 권한을 갖고 있는지 한눈에 보여줄 방법도 없고. (플랫폼 엔지니어링으로 풀어낼수는 있겠다)
OPA 는 정책 표현력이 훨씬 뛰어나지만 REGO 라는 DSL 을 배워야 하는데다, 배운다 한들 정책이 매니페스트에 사는 이상 개입 문제는 그대로 남는다.
반면 Ranger 를 고른 이유는 이랬다.
- 웹 UI 로 정책을 만들고 감사 로그까지 확인 할 수 있다. DevOps 엔지니어뿐 아니라 데이터 엔지니어나 보안 담당자도 직접 권한을 관리 할 수 있게 된다.
- LDAP 과 연계해서 유저·그룹 매핑을 그대로 가져올 수 있다.
- Hadoop 시절부터 오래 쓰여온 프로젝트라 성숙도가 있다.
물론 대가도 명확한데, 컴포넌트를 여러 개 띄우고 운영해야 한다는 것. 그리고 정책에 대한 GitOps 가 되지 않는것이 있다. (에이전트로 운영을 자동화하거나 DevOps 엔지니어링 일부를 위임하는경우에는 GitOps 가 되지 않는게 크리티컬 할수는 있겠다. 이건 개선해야 할 점으로 생각하고있다)
Apache Ranger 는 무엇인가
한 줄로 요약하면 여러 데이터 엔진에 걸친 인가 정책을 한곳에서 관리하는 도구다.
Ranger 는 Hadoop 생태계에서 태어났는데, HDFS 나 Hive, HBase, Kafka 같은 컴포넌트마다 권한 체계가 제각각이라 이걸 하나의 정책 모델과 하나의 UI 로 묶으려는 시도였고, 지금도 지원 플러그인 목록을 보면 그 계보가 그대로 보인다.
다만 목록이 Hadoop 에서 멈춰있지는 않아서 Elasticsearch, Kafka Schema Registry 같은것들도 있고 그 안에 Trino 와 Presto 도 들어있다.
즉 Hadoop 컴포넌트를 하나도 쓰지 않고 Trino 플러그인만 붙여서 쓰는 구성도 성립한다는 뜻인데, 우리가 한 것이 정확히 그것이다. 우리 환경에는 HDFS 도 YARN 도 Hive 서버도 없고, 오브젝트 스토리지 위에 Iceberg 테이블이 있고 그걸 Trino 가 읽는 형태다.

그런데 그런 구성을 설명해주는 문서가 마땅치 않았다. 공식 사이트는 사용법 대부분을 Confluence 위키로 넘기고 있는데, 그 위키에서 가장 먼저 눌러보게 되는 Ranger Installation Guide 는 마지막 수정이 2015 년이고 대상 버전이 0.4.0 이다. 우리가 설치한 것은 2.7.0 이고 지금 최신 릴리스는 2.8.0 인데.
문서가 다루는 플러그인도 HDFS, Hive, HBase, Knox, Storm 다섯 개라 Trino 는 아예 등장하지 않고, 딸려나오는 전제 버전들이 Hadoop 2.5.2 나 JDK 7 인 것을 보면 지금 환경과는 거리가 꽤 있다.
소스 트리를 봐도 사정이 비슷한데, plugin-trino 디렉터리에는 README 한 장이 없다. 결국 설정 항목 하나하나를 코드와 다른 플러그인의 사례에서 유추해가며 맞추는 식이 되었고, 문서가 도와주지 않는 구간이 꽤 길었다. 공식 Helm chart 도 없어서 최소한의 구성으로 매니페스트를 직접 작성해서 배포했다.
세 개의 컴포넌트
Ranger 를 설치한다고 할 때 실제로는 여러 개의 독립된 프로세스를 띄우는 일이 되는데. 이름이 다 비슷비슷해서 처음에 좀 헷갈렸지만, 역할로 나누면 셋이다.
| 컴포넌트 | 무엇인가 | 어디에 사는가 |
|---|---|---|
| Ranger Admin | 정책 저장소 + 웹 UI + REST API | 독립 서버 (백엔드 DB 필요) |
| Ranger Plugin | 실제로 허용/거부를 판정하는 라이브러리 | 데이터 엔진 프로세스 내부 |
| UserSync | LDAP 등에서 유저·그룹을 읽어 Admin 으로 밀어넣음 | 독립 프로세스 |
여기에 감사 로그를 저장할 곳이 하나 더 필요한데, Ranger 는 audit store 로 Solr 또는 Elasticsearch 를 지원해서 우리는 Solr 를 골랐다.

이 중에서 가장 오해하기 쉬운것이 Plugin 인데. 이름만 보면 Ranger 쪽에 붙는 무언가처럼 들리지만 실제로는 반대다. Plugin 은 데이터 엔진 쪽에 설치되는 라이브러리이고, Trino 의 경우 배포판에 jar 이 이미 들어있기 때문에(/usr/lib/trino/plugin/ranger/) 따로 설치할 것 없이 설정만 얹으면 된다.
access-control.name=ranger
ranger.service.name=trino
ranger.plugin.config.resource=/etc/trino/ranger-trino-security.xml,/etc/trino/ranger-trino-audit.xmlTrino helm chart 내 accessControl 값 예시
Trino 코디네이터가 쿼리를 받아 접근 권한을 확인해야 할 때 이 plugin 이 호출되어 허용 여부를 반환하는 형태인데, 즉 판정 자체는 Trino 프로세스 안에서 일어난다. 이 사실이 다음 이야기로 이어지는데.
정책 평가는 Admin 이 아니라 엔진 안의 로컬 캐시에서 일어난다
Ranger 를 처음 접했을 때는 이렇게 동작할 것이라고 짐작했었다. 유저가 쿼리를 던지면 Trino 가 Ranger Admin 에게 "이 사람 이거 봐도 되냐" 고 물어보고 답을 받아서 처리하는것 아닌가? 라는 그림이었는데, 실제로는 전혀 그렇지 않았다.
Trino 안의 plugin 은 주기적으로 Ranger Admin 을 폴링해서 정책 전체를 통째로 내려받아 로컬 파일에 캐싱해두고, 쿼리가 들어올 때는 그 로컬 캐시만 보고 판정한다. Admin 에게 물어보지 않는다.
ranger.plugin.trino.policy.pollIntervalMs = 1000
ranger.plugin.trino.policy.cache.dir = /tmp/ranger/policycacheadditionalConfigFiles 내 xml 값중 일부 예시
우리는 폴링 주기를 1초로 두고 있는데, 코디네이터 파드에 들어가서 캐시 디렉터리를 보면 아래와 같이 파일이 떨어져 있다.
/tmp/ranger/policycache/
trino_trino.json ← 정책 캐시
trino_trino_userstore.json ← 유저/그룹 캐시폴링이라고 해서 1초마다 정책 전체를 다시 받아오는것은 아니다. plugin 은 자기가 알고 있는 버전 번호를 함께 보내고 Admin 은 버전이 그대로면 변경 없음만 응답하기 때문에, 실제로 데이터가 오가는 것은 버전이 올라갔을 때 뿐이다.

이 설계에는 분명한 장점이 있는데. 쿼리 경로에 네트워크 왕복이 없으니 판정이 빠르고, Ranger Admin 이 죽어도 Trino 는 마지막으로 받아둔 정책으로 계속 동작한다. 권한 시스템이 쿼리 엔진의 단일 장애점이 되지 않는다는것은 꽤 중요한 성질이다.
대신 대가가 있다.
- 정책을 바꿔도 즉시 반영되지는 않는다. 폴링 주기만큼의 시차가 존재한다 (우리는 1초라 체감은 거의 없다).
- 그리고 더 중요한 것으로, 변경 감지가 오직 버전 번호 비교로만 이루어진다. 다시 말해 Admin 이 버전을 올려주지 않으면 plugin 은 그 변경을 영원히 알지 못한다.
두 번째 항목은 뒤에서 다룰 Ranger 운영 과정의 문제들과 이어지는 부분이다.
user, group, role 그리고 Ranger DB 라는 관문
Ranger 정책에서 권한을 부여받는 주체는 세 종류인데, user 와 group, 그리고 role 이다.
- user : 개별 사용자. 우리는 이메일 주소를 그대로 유저명으로 쓴다.
- group : 사용자의 묶음. LDAP 그룹이 그대로 동기화되어 들어온다.
- role : user 와 group 을 담을 수 있는 Ranger 고유의 개념이다. LDAP 에는 대응하는것이 없고 Ranger 안에서만 존재한다. "분석가", "일반 사용자" 처럼 권한 묶음에 이름을 붙이고 싶을 때 쓴다.

여기서 반드시 걸리는 함정이 하나 있는데.
Trino 가 인증을 통과시킨 멀쩡한 유저라 하더라도 Ranger 쪽에 해당 유저 레코드가 없으면 정책의 user 필드와 매칭 될 일이 없고, 애초에 정책을 만들 때 UI 에서 그 유저를 선택 할 수조차 없다. 그룹도 마찬가지다.
인증을 통과했으면 권한도 당연히 따라오는것 아닌가? 싶지만, 인증과 인가가 별개라는 앞의 이야기가 여기서 실감나게 다가오는 지점이다.
그래서 UserSync 라는 별도 컴포넌트가 존재하는 것인데, 이 친구의 유일한 임무는 LDAP 에 있는 유저와 그룹을 읽어서 Ranger Admin 의 DB 로 계속 밀어넣는 일이고, 단방향이라 반대 방향으로는 아무것도 흐르지 않는다.
그런데 이 구조에는 사각지대가 하나 있는데, LDAP 에 존재하지 않는 유저다. Redash 나 Airflow 같은 서비스 계정은 회사 구글 계정이 아니라 Trino 자체 password 인증으로 접속하는데, 이런 계정은 usersync 가 알 방법이 없다. 그래서 해당 유저들은 Ranger UI 나 REST API 로 직접 만들어줘야 한다. (UserSync 와 동기화하는 LDAP 레벨에서 이런 서비스어카운트가 존재하도록 하면 우아하게 풀어낼 수 있다. 지금은 Google Workspace Secure LDAP 을 무임승차중이라, 해당 방안은 적용하지 않았다)
curl -u admin:{admin_password} -X POST \
"https://ranger.example.com/service/xusers/secure/users" \
-H "Content-Type: application/json" \
-d '{
"name": "some_batch_job",
"userSource": 1,
"status": 1,
"isVisible": 1,
"userRoleList": ["ROLE_USER"]
}'userSource: 1 이 외부 소스 유저라는 표시인데, UI 에서 그냥 만들면 Internal 로 생성된다. 여기서 password 를 뭘로 넣든 상관없는것이, Ranger 는 이 유저로 로그인시킬 생각이 없고 오직 username 매칭만 하기 때문이다. 인가 전용 레코드인 셈이다.
그룹과 관련해서는 선택지가 하나 더 있었는데. Trino 에도 자체 group provider 가 있어서 유저를 그룹에 매핑 할 수 있고, Ranger plugin 은 이 둘을 어떻게 조합할지 설정으로 정하게 되어있다.
<property>
<name>ranger.plugin.trino.use.rangerGroups</name>
<value>true</value>
</property>
<property>
<name>ranger.plugin.trino.use.only.rangerGroups</name>
<value>true</value>
</property>우리는 둘 다 true 인데, 즉 그룹 정보의 출처를 Ranger 하나로 단일화하고 Trino 쪽 그룹 매핑은 아예 보지 않는 형태. 권한의 근거를 한곳으로 모으는 편이 관리하기 명확하다고 판단해서 이렇게 정했는데, 이 선택은 나중에 usersync 가 그룹 멤버십을 잘못 건드렸을 때 완충장치가 하나도 없다는 뜻이기도 했다.
유저 라이프사이클은 LDAP 이 관리한다
UserSync 를 조금 더 이야기해두는것이 좋겠다.
왜 하필 LDAP 인가? 사내 유저의 소속과 권한, 그리고 재직 여부까지를 관리하는 단일 출처가 어딘가에는 있어야 하는데, 디렉터리 서비스가 원래 그 역할을 하도록 만들어진 것이다.
입사하면 계정이 생기고 팀을 옮기면 그룹이 바뀌고 퇴사하면 비활성화되는 흐름이 한곳에서 일어나면, 그걸 참조하는 시스템들은 각자 유저 목록을 따로 들고 있을 필요가 없어진다.
그래서 사내 도구들은 대체로 디렉터리에서 유저와 그룹 정보를 주기적으로 받아와 자기 쪽 계정의 라이프사이클을 맞추는 형태로 동작한다.
Ranger 의 UserSync 도 정확히 그 일을 하는 컴포넌트인데, 앞에서 이야기한 Ranger DB 에 유저가 있어야 판정이 된다는 제약을 자동으로 메꿔주는 장치라고 보면 된다. 정책은 사람이 UI 에서 만들지만 그 정책이 가리킬 유저와 그룹은 UserSync 가 계속 채워넣는 형태.
우리는 이미 회사 계정으로 쓰고 있던 Google Workspace 의 Secure LDAP 을 그대로 붙였다. 디렉터리 서버를 새로 세우지 않아도 되고 유저와 그룹의 원본이 이미 거기에 있으니, 추가 인프라 없이 기존 단일 출처를 그대로 이어받을 수 있다는 점이 컸다.
설정 자체는 install.properties 한 장으로 끝난다. 표준 LDAP 을 가정하고 만들어진 항목들을 Google Workspace 쪽 스키마에 맞춰 바꿔주는 일인데, 어떤 속성이 다른지가 여기서 그대로 드러난다. (bind 계정이나 keystore 비밀번호처럼 민감한 값은 ExternalSecrets 로 주입해서 템플릿 자리만 남겨뒀다)
SYNC_SOURCE=ldap
SYNC_INTERVAL=60
SYNC_LDAP_URL=ldaps://ldap.google.com:636
SYNC_LDAP_BIND_DN={{ .ldap_user }}
SYNC_LDAP_BIND_PASSWORD={{ .ldap_password }}
SYNC_LDAP_SEARCH_BASE=dc=example,dc=com
# 표준 LDAP 운영 속성이 없어서 delta sync 를 끈다
SYNC_LDAP_DELTASYNC=false
# user: objectClass 는 person, 유저명은 mail 속성을 그대로 쓴다
SYNC_LDAP_USER_SEARCH_BASE=ou=Users,dc=example,dc=com
SYNC_LDAP_USER_OBJECT_CLASS=person
SYNC_LDAP_USER_NAME_ATTRIBUTE=mail
SYNC_LDAP_USER_GROUP_NAME_ATTRIBUTE=memberOf
# group: memberUid 가 아니라 member 속성을 본다
SYNC_GROUP_SEARCH_BASE=ou=Groups,dc=example,dc=com
SYNC_GROUP_OBJECT_CLASS=groupOfNames
SYNC_GROUP_NAME_ATTRIBUTE=cn
SYNC_GROUP_MEMBER_ATTRIBUTE_NAME=member
SYNC_GROUP_USER_MAP_SYNC_ENABLED=true
SYNC_LDAP_USERNAME_CASE_CONVERSION=lower
SYNC_LDAP_GROUPNAME_CASE_CONVERSION=lower
SYNC_PAGED_RESULTS_ENABLED=true
SYNC_PAGED_RESULTS_SIZE=500
# mTLS 클라이언트 인증서. Secure LDAP 은 이게 필수다
SYNC_LDAP_SSL_KEYSTORE=/ldap-cert/ldap-keystore.p12
SYNC_LDAP_SSL_KEYSTORE_PASSWORD={{ .keystore_password }}
# LDAP 그룹 멤버십으로 Ranger 관리자 권한을 부여할 수도 있다
GROUP_BASED_ROLE_ASSIGNMENT_RULES=ROLE_SYS_ADMIN:g:platform-team대신 대가가 있었는데, 표준 LDAP 이 정의하는 운영 속성 일부를 제공하지 않아서 변경분만 가져오는 delta sync 를 쓸 수 없고 그래서 매 사이클마다 전체를 다 훑는 full sync 로 돌고 있다.
왜 그런지는 아래에서 코드까지 들여다볼 예정인데, 여기서는 결론만 적어두면 동기화 대상 규모가 한 사이클을 부담스럽게 할 정도는 아니라고 판단해서 지금은 이렇게 두고 있다.
언젠가 규모가 커지거나 동기화 주기를 더 촘촘히 가져가야 할 때가 오면 표준 속성을 온전히 제공하는 디렉터리로 옮기는것을 검토하게 될 텐데, 다행히 그때 드는 비용은 크지 않다.
Ranger 쪽에서 바뀌는것은 LDAP URL 과 검색 필터 정도이고 유저와 그룹의 이름 체계를 그대로 유지하면 정책은 손댈 필요도 없으니, 지금은 구현 편의성과 추가 인프라가 필요하지 않다는 이점이 delta sync 를 못 쓰는 제약보다 크다고 봤다.
동기화는 1시간 주기로 돌고 있고, 각 사이클이 무엇을 얼마나 바꿨는지는 감사 화면에 그대로 남는다.

사이클 하나를 열어보면 어떤 조건으로 LDAP 을 훑었는지까지 확인 할 수 있다.

우리 구성
거창할 것은 없고, Ranger Admin 과 UserSync 를 각각 파드로 띄우고 정책 저장소로 관리형 PostgreSQL, 감사 로그 저장소로 Solr 를 붙인 형태다. LDAP 소스는 Google Workspace Secure LDAP 이고, Trino 쪽은 배포판에 내장된 plugin 을 설정으로 켜둔 형태.
몇가지 결정만 짚어두면 이렇다.
Solr 는 단일 파드로 띄웠다. 감사 로그 조회가 주 용도라 처음부터 고가용성이 필요하지 않다고 봤고, ZooKeeper 를 끼워넣는 SolrCloud 모드는 운영 부담이 훨씬 커지는데 얻는 것이 없었다. 필요해지면 그때 관리형 검색 서비스로 옮기는 편이 낫다고 판단했다.
감사 로그를 Solr 로 보내는 설정은 Ranger Admin 이 아니라 Trino 쪽 plugin 이 들고 있다. 판정을 plugin 이 하니 기록을 남기는 주체도 plugin 인 셈.
<configuration>
<property>
<name>xasecure.audit.is.enabled</name>
<value>true</value>
</property>
<property>
<name>xasecure.audit.solr.is.enabled</name>
<value>true</value>
</property>
<property>
<name>xasecure.audit.solr.solr_url</name>
<value>http://ranger-solr.ranger.svc.cluster.local:8983/solr/ranger_audits</value>
</property>
</configuration>Ranger UI 로그인도 LDAP 으로 붙여뒀다. authentication_method=LDAP 으로 두면 회사 구글 계정의 아이디와 비밀번호로 그대로 로그인이 되기 때문에, 권한을 관리할 사람들에게 별도 계정을 발급하고 관리할 필요가 없어진다.
다만 Secure LDAP 이 클라이언트 인증서를 요구해서 keystore 를 만들어 마운트해줘야 하는데, 이 부분은 삽질이 꽤 있었다.
Admin 과 UserSync 는 둘 다 컨테이너 이미지를 직접 빌드해서 쓴다. 왜 그래야 했는지는 아래에서 이야기하겠다.
SSO 는 붙이지 못했다
사실 Ranger UI 접근도 SSO 로 통일하고 싶었다. 사내 다른 도구들은 대부분 SSO 로 붙어있어서 Ranger 만 아이디와 비밀번호를 따로 입력하는것이 계속 걸렸는데, 결론부터 이야기하면 방법을 찾지 못했다.
Ranger 에 SSO 설정 자체는 있는데.
<property>
<name>ranger.sso.enabled</name>
<value>false</value>
</property>
<property>
<name>ranger.sso.providerurl</name>
<value>https://127.0.0.1:8443/gateway/knoxsso/api/v1/websso</value>
</property>
<property>
<name>ranger.sso.publicKey</name>
<value></value>
</property>그런데 providerurl 의 기본값을 보면 이 SSO 가 무엇을 전제하고 있는지가 그대로 드러난다. Apache Knox 다.
인증 필터 구현을 열어보면 Knox 가 발급한 hadoop-jwt 쿠키를 읽어서 설정에 박아둔 RSA public key 하나로 서명을 검증하는 구조로 되어있고, 쿠키가 없으면 providerurl 로 리다이렉트시킨다.
OIDC discovery 도 없고 JWKS 엔드포인트를 조회하지도 않기 때문에 키 로테이션을 따라갈 방법도 없다. 다시 말해 일반적인 IdP 를 Ranger 에 직접 물리는 경로는 사실상 없다.
그러니까 SSO 를 쓰려면 앞단에 Knox 를 세우고 Knox 가 IdP 와 붙게 한 다음 Knox 가 발급한 JWT 를 Ranger 가 받는 형태가 되는것인데, Hadoop 생태계 컴포넌트를 하나 더 운영해야 한다는 뜻이기도 하다.
Ranger UI 를 실제로 쓰는 사람이 몇 명 되지 않는 지금 상황에서는 굳이 싶어서 LDAP 인증으로 남겨뒀다. (Knox 를 실제로 붙여보지는 않았으니 이건 코드와 문서만 보고 내린 판단이다)
정책은 어떻게 생겼나
Ranger 의 정책은 리소스 기반인데, Trino 서비스를 Ranger 에 등록하면 기본 정책 13개가 자동으로 만들어진다. 해당 목록을 보는것만으로 Trino 에서 통제 가능한 대상이 무엇인지 대략 파악이 되는편이다.
데이터 계층은 catalog → schema → table → column 으로 내려가는데, 여기에 각각 부여 할 수 있는 권한이 조금씩 다르다.
| 리소스 | 사용 가능한 권한 |
|---|---|
| catalog | select, create, drop, show, all |
| schema | select, create, drop, show, all |
| table | select, insert, update, delete, show, drop, create, all |
| column | select, all |
데이터 계층 말고도 function, procedure, sessionproperty, systemproperty, role 같은 통제 대상이 더 있는데, 그중 두 개가 특히 눈에 띄었다.
queryid: 쿼리를 실행하고 중지하는 권한이다. 다른 권한과 다르게execute하나만 존재한다.trinouser:impersonate권한이 붙는 리소스인데, 말 그대로 다른 사용자로 가장 할 수 있는 권한이다. 이게 있어야 웹 UI 에서 남의 쿼리를 볼 수 있고, 없는 유저는 자기가 돌린 쿼리만 보이고 자기 쿼리만 kill 할 수 있다.
운영자 입장에서는 trinouser 의 impersonate 를 모든 유저에 대해 열어둬야 클러스터 전체 쿼리를 들여다볼 수 있고, 반대로 일반 사용자에게는 이걸 주면 안된다. 처음에는 "왜 나는 쿼리 목록이 텅 비어보이지?" 라는 생각에 한참을 헤맸던 부분이다.
impersonate 가 실제로 어디에 쓰였는지 하나만 적어두면 BI 도구 연동인데, Redash 처럼 데이터소스마다 계정 하나로 엔진에 붙는 도구가 문제가 된다.
그대로 두면 Trino 입장에서는 모든 쿼리가 redash 라는 서비스 계정 하나로 들어오는 것처럼 보이기 때문에, 누가 무엇을 조회했는지 감사 로그에 남지 않고 개인이나 팀 단위로 권한을 나눌 방법도 없어진다.
Trino 에는 이걸 풀 수 있는 impersonation 이 있는데. 접속 자체는 서비스 계정으로 하되 이 쿼리는 누구 것이라고 별도로 알려주면 Trino 가 그 사람 권한으로 처리하는 방식인데, 정작 Redash 쪽에 그 옵션이 없었다.
그래서 query runner 에 impersonation 설정을 추가하는 Redash#7605 를 올렸고 머지되었다. 데이터소스 설정에서 켜고 끌 수 있고 어떤 값을 넘길지도 고를 수 있게 해뒀는데, 기본은 이메일이고 API 키처럼 이메일이 없는 주체는 이름으로 떨어진다.
이제 Redash 에서 실행한 쿼리는 로그인한 회사 계정 이름으로 Trino 에 도착한다. Ranger 쪽에서는 거기에 맞춰 정책을 하나 만들어두면 되는데, redash 서비스 계정에게 회사 도메인 유저를 impersonate 할 권한을 주는 형태.
정책에는 allow 조건과 deny 조건을 각각 넣을 수 있고 각 조건에서 예외(exclude)를 지정하는것도 가능하다. 그리고 각 정책에는 우선순위가 있어서 Override 로 지정한 정책이 일반 정책을 이기게 되는데, 예컨대 그룹 전체에 allow 를 주고 그중 한 명만 막고 싶다면 일반 deny 로는 안 되고 Override deny 여야 한다.

컬럼 단위 제어에는 알아둬야 할 부작용이 하나 있는데. 특정 컬럼을 막아두면 그 테이블에 SELECT * 를 날렸을 때 에러가 나기 때문에, 막힌 컬럼을 빼고 명시적으로 컬럼을 나열해줘야 한다. 생각해보면 당연한 동작이지만 쓰는 사람 입장에서는 갑자기 SELECT * 가 안 되는 것이라 별도 안내가 필요했다.
실무에서는 정책을 유저 단위로 만들기보다 role 과 group 단위로 만드는 편이 훨씬 관리하기 좋았다.
개인에게 직접 권한을 주기 시작하면 퇴사자 정리나 팀 이동 때마다 정책을 일일이 손봐야 하는데, 그룹 기반이면 LDAP 쪽에서 그룹만 옮겨주면 자동으로 따라온다. 다만 이 편리함은 usersync 가 그룹을 정확히 동기화해준다는 전제 위에 서 있다.
그리고 정책이 실제로 어떻게 작동했는지는 감사 로그에서 확인하는데. 허용이든 거부든 판정 결과가 남기 때문에, 권한 때문에 쿼리가 실패했다는 문의가 오면 여기부터 열어보게 된다.

도입하면서 알게 된 것들
여기까지가 Ranger 라는 도구의 생김새다. 그림만 보면 단순하고 개념 자체도 딱히 어렵지 않은데, 문제는 이걸 Hadoop 이 없는 환경에 붙이려고 할 때 문서가 도와주지 않는다는 점이었다. 실제로 부딪힌 것들을 적어보면 이렇다.
UserSync 는 공식 컨테이너 이미지가 없다. Admin 은 도커 허브에 이미지가 올라와 있는데 usersync 는 없어서 소스에서 직접 빌드해야 했고, 그 빌드가 순순히 되지 않았다.
Google Workspace Secure LDAP 은 delta sync 를 쓸 수 없다. usersync 는 매 사이클마다 전체를 다 훑지 않으려고 변경분만 가져오는 delta sync 를 지원하는데, 문제는 이것이 어떻게 구현되어 있느냐인데. 2.7.0 의 `LdapUserGroupBuilder.getUsers()` 를 보면 이렇게 되어있다.
if (groupUserTable.rowKeySet().size() != 0 || !config.isDeltaSyncEnabled() || (computeDeletes)) {
// Fix RANGER-1957: Perform full sync when there are updates to the groups or when incremental sync is not enabled
deltaSyncUserTime = 0;
deltaSyncUserTimeStamp = dateFormat.format(new Date(0));
}
extendedUserSearchFilter = "(objectclass=" + userObjectClass + ")(|(uSNChanged>=" + deltaSyncUserTime + ")(modifyTimestamp>=" + deltaSyncUserTimeStamp + "Z))";delta sync 가 꺼져있으면(!config.isDeltaSyncEnabled()) 기준 시각을 epoch 으로 되돌려서 전체를 다 가져오겠다는 의도까지는 좋은데, 바로 다음 줄에서 필터 자체는 무조건 붙인다.
그러니까 delta sync 를 끄더라도 검색 필터에는 (|(uSNChanged>=0)(modifyTimestamp>=19700101...Z)) 가 그대로 들어간다.
uSNChanged 는 Active Directory 의 속성이고 modifyTimestamp 는 표준 LDAP 운영 속성인데, Google Workspace Secure LDAP 은 이 두 개를 다 내주지 않는다. 속성이 아예 없으니 저 OR 조건을 만족하는 엔트리도 하나도 없고 검색 결과가 0건이 되는데, 유저가 한 명도 동기화되지 않는것이다.
그러면 delta sync 를 끄면 되지 않나 싶은데, install.properties 에 SYNC_LDAP_DELTASYNC=false 를 넣어도 증상이 그대로였는데. 설치 스크립트인 `setup.py` 를 따라가보니 이런 코드가 있었다.
elif (syncSource == SYNC_SOURCE_LDAP):
ret['ranger.usersync.ldap.deltasync'] = "true"install.properties 를 XML 설정으로 변환하는 지점인데, LDAP 소스이기만 하면 사용자가 무슨 값을 넣었든 무조건 `true` 로 덮어쓴다. 설정 파일에 적어둔 false 는 애초에 읽히지도 않았던 것이다.
결국 이 줄이 install.properties 의 값을 존중하도록 고치고, 필터 쪽도 delta sync 가 꺼져 있으면 저 조건을 아예 붙이지 않도록 분기를 넣었다. (사내에 별로 포크버전을 만들어서 직접 빌드해서 사용중이다)
정상적으로 재직 중인 유저가 어느 날 갑자기 권한을 잃는다. 조직 개편으로 LDAP 의 OU 가 바뀌면 usersync 가 그 사람을 "삭제된 유저" 로 오판하는데, Ranger 의 오래된 설계 결함이고, 업스트림에 RANGER-5110 이 올라와 있지만 1년 반째 머지되지 않고 있다. 우리가 해당 버그를 정면으로 맞게 되었다.
고친 것들, 그리고 업스트림
이런 것들이 쌓이다 보니 업스트림 2.7.0 을 그대로 쓰지 못하고 별도 브랜치를 유지하게 되었는데, 크게 보면 세 갈래였다.
- delta sync 를 실제로 끌 수 있게 하는 것. 위에서 이야기한 설치 스크립트의 하드코딩과 LDAP 검색 필터 분기다.
- usersync 가 놓치고 있던 것들. 그룹 멤버십이 바뀌어도 userstore 버전이 올라가지 않던 문제, 삭제된 그룹이 hidden 으로 마킹되지 않던 조건 오타 같은것들.
우리만 겪을 문제가 아니라고 생각해서 업스트림에도 올려봤다.
| 내용 | PR | 상태 |
|---|---|---|
| 삭제된 그룹이 hidden 으로 마킹되지 않던 조건 오타 | RANGER-5461 | 머지됨 |
| 그룹 멤버십 변경 시 userstore 버전이 갱신되지 않던 문제 | RANGER-5463 | 머지됨 (master, 2.9) |
| delta sync 하드코딩과 불필요한 LDAP 필터 | RANGER-5466 | 리뷰 대기중 |
두 번째 건은 우리 환경이 특이해서 겪는 문제인가 싶었는데, 같은 버그를 맞고 있다는 다른 사용자의 코멘트가 PR 에 달리기도 했다. 메인테이너가 master 와 2.9 브랜치에 반영해주었다. (PR 자체는 closed 로 보이지만 코드는 들어가 있다)
정리
- Trino 는 상태를 거의 남기지 않는다. 쿼리 기록도 인증 정보도 메모리에만 있어서, 남겨야 하는 것은 event listener 로 빼내야 하고 권한 역시 외부에 두고 참조하는 구조가 된다.
- 인증과 인가는 다른 일이다. Trino 가 "누구인가" 까지 알려주면 Ranger 가 "무엇을 할 수 있는가" 를 판단한다.
- 정책을 매니페스트 밖으로 꺼내는 것이 Ranger 를 고른 이유다. file 이나 OPA 는 권한 변경마다 인프라 팀이 개입해야 하지만 Ranger 는 UI 로 위임 할 수 있다.
- 정책 평가는 Ranger 에 물어보는 것이 아니라 엔진 안의 로컬 캐시에서 일어난다. 빠르고 장애에 강한 대신 변경 전파가 전적으로 버전 번호에 의존한다.
- Ranger 가 모르는 유저와 그룹에는 정책을 적용 할 수 없다. LDAP 에 없는 서비스 계정은 직접 만들어줘야 한다.
- 업스트림을 그대로 쓰지는 못했다. Google Workspace Secure LDAP 조합에서는 delta sync 가 성립하지 않는데 그 옵션이 설치 스크립트에서 하드코딩되어 있었고, 컨테이너 환경을 전제하지 않은 로그 처리도 손을 봐야 했다. 고친 것 중 남들도 겪을 만한 것들은 업스트림에 올렸고 두 건은 반영되었다.
- 정책은 그룹과 role 기준으로 설계하는 편이 좋다.