보안과 접근 제어: LDAP, OAuth2, 파인-그레인드 ACL
Trino 보안이 어려운 이유
Trino는 MySQL, Iceberg, Kafka, PostgreSQL 등 여러 데이터소스를 단일 SQL로 접근하는 쿼리 페더레이션 엔진이다. 데이터가 한 곳에 있지 않고 여러 시스템에 분산돼 있다는 것은 곧, 보안 제어를 어느 한 데이터베이스 레이어에서만 걸 수 없다는 뜻이다.
Trino의 보안은 크게 두 가지 질문으로 나뉜다.
- 인증(Authentication): 이 사용자가 정말 그 사람인가?
- 인가(Authorization): 그 사람이 이 데이터를 볼 자격이 있는가?
둘은 독립적으로 구성하지만, 인증이 먼저 통과되어야 인가가 의미를 갖는다. 그리고 둘 모두 TLS 위에서만 제대로 동작한다.
모든 보안의 전제 조건: TLS
Trino는 LDAP, OAuth2, PASSWORD 인증을 사용할 때 반드시 TLS(HTTPS)를 활성화해야 한다. 평문 HTTP 환경에서는 비밀번호와 토큰이 네트워크에 노출된다.
config.properties에 다음을 추가한다.
http-server.https.enabled=true
http-server.https.port=8443
http-server.https.keystore.path=/etc/trino/keystore.jks
http-server.https.keystore.key=keystore_password클러스터 내부 워커-코디네이터 통신에도 공유 시크릿(internal communication secret)을 설정해야 한다.
internal-communication.shared-secret=<무작위 긴 문자열>이 두 설정 없이 PASSWORD 계열 인증을 활성화하면 Trino가 시작을 거부한다.
인증: LDAP
LDAP 인증은 기업 내 Active Directory나 OpenLDAP 서버를 Trino의 사용자 검증 백엔드로 사용한다. 사용자가 CLI나 JDBC로 접속할 때 입력한 비밀번호를 LDAP 서버에 바인드 시도해 검증한다.
설정 파일
etc/config.properties (코디네이터)
http-server.authentication.type=PASSWORDetc/password-authenticator.properties
password-authenticator.name=ldap
ldap.url=ldaps://ldap.company.com:636
ldap.ssl.truststore.path=/etc/trino/ldap-server.pem
ldap.user-bind-pattern=uid=${USER},ou=users,dc=company,dc=com${USER}는 로그인 시 입력한 사용자 이름으로 치환된다.
그룹 멤버십 제한
특정 그룹에 속한 사용자만 접속하게 하려면 서비스 계정으로 LDAP를 검색해야 한다.
ldap.bind-dn=cn=trino-svc,ou=service,dc=company,dc=com
ldap.bind-password=<서비스계정비밀번호>
ldap.user-base-dn=ou=users,dc=company,dc=com
ldap.group-auth-pattern=(&(objectClass=groupOfNames)(cn=trino-users)(member=uid=${USER},ou=users,dc=company,dc=com))LDAP 검색 필터는 LDAP 서버 스키마에 맞춰야 한다. objectClass나 그룹 속성명이 서버마다 다르므로 ldapsearch로 먼저 확인하는 것이 좋다.
인증: OAuth2 / OIDC
OAuth2 인증은 외부 IdP(Identity Provider — Keycloak, Okta, Google, Azure AD 등)를 통해 사용자 인증을 위임한다. 브라우저 기반 Web UI 로그인에 특히 유용하며, PKCE(Proof Key for Code Exchange) 흐름을 통해 CLI/JDBC도 지원한다.
동작 흐름
Web UI 요청 → 코디네이터
302 Redirect → IdP 로그인
Keycloak/Okta → 인증 코드
/oauth2/callback → 토큰 교환
Access + ID Token
=oauth2 authentication.oauth2.issuer
=https://idp/realm/company authentication.oauth2.client-id
=trino authentication.oauth2.client-secret
=<secret>
설정
etc/config.properties
http-server.authentication.type=oauth2
http-server.https.enabled=true
http-server.https.port=8443
http-server.authentication.oauth2.issuer=https://keycloak.company.com/realms/data
http-server.authentication.oauth2.client-id=trino
http-server.authentication.oauth2.client-secret=<클라이언트시크릿>OIDC 호환 IdP는 <issuer>/.well-known/openid-configuration에 메타데이터를 게시한다. Trino는 이 엔드포인트를 자동으로 읽어 authorization_endpoint, token_endpoint, jwks_uri 등을 가져온다. 따라서 개별 URL을 다 입력할 필요가 없다.
리프레시 토큰 활성화
http-server.authentication.oauth2.refresh-tokens=true
http-server.authentication.oauth2.scopes=openid,offline_accessoffline_access 스코프를 요청해야 IdP가 리프레시 토큰을 발급한다. 리프레시 토큰이 없으면 액세스 토큰 만료 시 재인증이 필요하다.
LDAP vs OAuth2 선택 기준
| 기준 | LDAP | OAuth2/OIDC |
|---|---|---|
| 기존 인프라 | Active Directory가 있는 경우 | SSO/IdP가 이미 있는 경우 |
| 브라우저 로그인 | 지원 (기본 인증) | 지원 (리다이렉트 흐름) |
| MFA 지원 | 어려움 | IdP에서 자연스럽게 지원 |
| 비밀번호 관리 | LDAP 서버 | IdP (Keycloak, Okta 등) |
| CLI 지원 | 기본 지원 | PKCE 흐름 필요 |
인가: 파일 기반 접근 제어 (Fine-Grained ACL)
Trino는 인증된 사용자가 어떤 카탈로그, 스키마, 테이블, 컬럼에 접근할 수 있는지를 JSON 파일로 정의한다.
설정 활성화
etc/access-control.properties
access-control.name=file
security.config-file=/etc/trino/access-control/rules.jsonrules.json 경로는 절대 경로 또는 코디네이터 기준 상대 경로다.
규칙 구조
{
"catalogs": [
{
"user": "admin",
"catalog": ".*",
"allow": "all"
},
{
"group": "data-engineers",
"catalog": "iceberg",
"allow": "read-only"
},
{
"catalog": ".*",
"allow": "none"
}
],
"schemas": [
{
"user": "analyst_.*",
"catalog": "iceberg",
"schema": "warehouse",
"owner": false
}
],
"tables": [
{
"user": "reporting_user",
"catalog": "iceberg",
"schema": "warehouse",
"table": "orders",
"privileges": ["SELECT"]
}
]
}규칙은 위에서 아래로 첫 번째 매칭 규칙이 적용된다. 마지막에 "allow": "none" 같은 기본 거부 규칙을 두지 않으면 의도치 않게 모든 사용자가 접근할 수 있게 된다.
컬럼 마스킹과 행 필터링
테이블 규칙에서 특정 컬럼을 숨기거나 마스킹할 수 있다.
"tables": [
{
"user": "analyst",
"catalog": "iceberg",
"schema": "warehouse",
"table": "customers",
"privileges": ["SELECT"],
"columns": [
{
"name": "email",
"allow": false
},
{
"name": "phone",
"mask": "regexp_replace(phone, '\\d{4}