团队把 Ambari 集群的 Kerberos 开关打开之后,老 HDP 栈常见的那套问题一个接一个冒出来。其中最让我头疼的不是 HDFS,也不是 Hive,反而是看起来最不起眼的 Trino Web UI。Trino 的 CLI 可以通过 Kerberos 正常连接,但浏览器访问 Web UI 要么一直 401,要么干脆空白。后来把 Knox 加进来做统一入口,才算把这条链路理顺。这篇文章就是把当时踩过的坑、试过的方案、最后落地的配置完整记录下来,给同样在 Ambari 开启 Kerberos 之后需要把 Trino Web UI 交给 Knox 的团队一个可复现的参照。
1. 先说清楚:Trino Web UI 在 Kerberos 集群里到底卡在哪
1.1 Trino 的 HTTP 服务认证逻辑
Trino 的 Web UI 本质上不是独立应用,而是 Trino HTTP Server 上的一组静态资源加 REST API。默认监听 8080 端口,/ui/路径返回前端页面,/v1/statement、/v1/info这些路径则提供服务状态和查询接口。
集群没有开 Kerberos 的时候,这个 HTTP Server 默认不认证,任何人只要能访问 8080 端口就能打开查询页面。Ambari 开启 Kerberos 之后,Trino 如果配置了http-server.authentication.type=KERBEROS,那么 HTTP Server 会启用 SPNEGO 认证。服务端以自己的 HTTP principal 作为身份,通常是HTTP/trino-host.example.com@EXAMPLE.COM,所有浏览器请求必须携带协商好的 Kerberos 票据,否则返回401 Unauthorized。
这个机制本身没有问题,但对使用者极不友好。浏览器需要额外配置auth_negotiate白名单,主机名必须和 principal 完全匹配,静态资源请求也会触发 SPNEGO,经常一个页面加载出一堆 401 记录。Trino CLI 和 JDBC 客户端走的是标准 Kerberos 认证流程,配置好 keytab 和 krb5.conf 之后通常一次成功,但 Web UI 的用户往往不是愿意折腾浏览器底层配置的人。
1.2 为什么"You have to have a ticket"这条路走不通
我在现场排障时遇到过最典型的场景:DBA 把 Kerberos 票据申请好后,用浏览器打开http://trino-host:8080/ui/,Chrome 直接弹一个登录框,输入域账号密码之后还是 401。原因往往不是密码错误,而是浏览器根本不认识 SPNEGO 协商流程。
Trino 的 Kerberos 认证走的是 HTTP Negotiate 协议。服务端返回WWW-Authenticate: Negotiate,浏览器需要用本地的 Kerberos ticket 构造 SPNEGO token 回传。这个过程中,如果浏览器所在机器没有加入域,或者没有配置intranet站点的协商白名单,或者访问的地址和 Trino 的 service principal 中注册的 hostname 不一致,就会失败。对于只在内网用网页查询数据的人,这个门槛太高。
1.3 Knox 在中间扮演的角色
Knox 解决的核心问题是统一入口。用户不需要直接面对 Trino 的 8080,也不需要在浏览器上配 Kerberos,只需要访问https://knox-host:8443/gateway/default/trino-ui/ui/,Knox 替用户完成认证,再向后端 Trino 发起请求。这里的难点在于 Knox 不是透明代理,它有自己的认证、rewrite、header 处理逻辑,需要明确告诉它后端是什么,路径怎么映射,认证用什么方式。这也是很多人配完之后一脸懵的原因。
2. 方案取舍:三种接入方式的对比与选择
2.1 直连 Trino + 浏览器 SPNEGO
最朴素的方式,用户直接访问 Trino 8080 端口,浏览器走 Kerberos 协商。优点是少一层网关,不需要 Knox。缺点是整个浏览器访问链路的体验非常差,而且如果 Trino 被部署在边缘节点,8080 端口直接暴露在办公网有安全风险。在 Kerberos 开启的集群里,直连还要求每个访问用户的浏览器环境都正确配置,运维谈判成本极高。
2.2 Knox + Trino 纯 Kerberos 认证
理论上最"正统"的方案:用户访问 Knox,Knox 认证通过后,以 Kerberos 客户端身份向 Trino 发起 SPNEGO 协商。但这个方案在实操层面最麻烦。Knox 虽然支持 Kerberos 相关的认证 provider,但要让它自动为每个后端连接维护 SPNEGO token、处理协商循环,配置复杂度很高,且不同版本的 Knox 支持情况差异很大。我曾经试着在 Knox 的 topology 里加 SPNEGO 客户端配置,最后因为 Knox 自带的服务定义里没有一个能干净地处理 Negotiate 过程而放弃了。
2.3 Knox + Trino 双认证(KERBEROS + PASSWORD)
最终我采用的是这个方案。Trino 端开启KERBEROS,PASSWORD双认证,CLI、JDBC 这些服务间调用继续走 Kerberos,Web UI 的 HTTP 请求则通过 Knox 转发时携带 Basic Auth 凭据,使用 Trino 的 Password Authenticator 完成认证。
这个方案的优点在于:
- 不破坏已有 Kerberos 安全链路。
- 用户只感知 Knox 的统一认证入口。
- Knox 到 Trino 之间的认证机制简单可控,排障难度低。
安全上我做了限制:Trino 的 8080 端口只对 Knox 节点和运维网段开放,办公网用户只能访问 Knox 的 8443。如果你们安全要求很高,还可以在这个基础上做 IP 白名单,或者给 Knox 单独分配一个服务账号,不让它代替用户传递密码。
三种方式对比如下:
| 方案 | 用户操作复杂度 | 配置复杂度 | 后端暴露风险 | 推荐程度 |
|---|---|---|---|---|
| 直连 Trino + SPNEGO | 高,依赖浏览器配置 | 低 | 高 | 不推荐 |
| Knox + 纯 Kerberos | 低 | 很高,依赖版本支持 | 较低 | 看情况 |
| Knox + 双认证 | 低 | 中 | 低,可加白名单 | 推荐 |
3. 环境准备:Principal、Keytab、密码文件一个都不能少
3.1 KDC 上为 Trino 准备 principal 和 keytab
在 Ambari 开启 Kerberos 的环境里,KDC 通常已经存在,我这边是 MIT KDC。要做的第一步是给 Trino 的 HTTP Server 创建 principal。
使用HTTP作为 service name,因为 Trino 默认http-server.authentication.krb5.service-name=HTTP。假设 Trino 安装在trino-host.example.com,realm 是EXAMPLE.COM,命令如下:
sudo kadmin.local -q "addprinc -randkey HTTP/trino-host.example.com@EXAMPLE.COM" sudo kadmin.local -q "ktadd -k /tmp/trino.keytab HTTP/trino-host.example.com@EXAMPLE.COM"然后把 keytab 拷到 Trino 节点上,并设置正确的属主和权限:
sudo scp /tmp/trino.keytab trino-host:/etc/trino/conf/ sudo chown trino:hadoop /etc/trino/conf/trino.keytab sudo chmod 400 /etc/trino/conf/trino.keytab这里有个细节:Trino 的 HTTP principal 的 hostname 必须和浏览器访问的 hostname 一致。如果你希望通过 Knox 转发,其实 Trino 不需要对外开放,但 principal 里的 hostname 仍然要是本机实际解析名,否则 Trino 启动时拉取 keytab 会报 principal 不匹配。
3.2 Trino 端开启双认证
编辑 Trino 的config.properties,一般在/etc/trino/conf/config.properties。关键配置如下:
http-server.authentication.type=KERBEROS,PASSWORD http-server.authentication.krb5.service-name=HTTP http-server.authentication.krb5.keytab=/etc/trino/conf/trino.keytab http-server.authentication.krb5.principal=HTTP/trino-host.example.com@EXAMPLE.COM password-authenticator.name=file file.password-file=/etc/trino/password.dbhttp-server.authentication.type同时保留KERBEROS和PASSWORD,这样 Trino 对同一端口既接受 Kerberos 协商,也接受 HTTP Basic 认证。Trino 在检测到请求头里的 Authorization 类型后会自动进入不同认证流程,不需要额外配置两个端口。
生成密码文件可以使用 htpasswd,Trino 的 file password authenticator 要求 bcrypt 格式:
sudo htpasswd -B -C 10 -c /etc/trino/password.db trino_service这里我建了一个专用服务账号trino_service,后面 Knox 转发时统一用它。注意-B参数指定 bcrypt,-C 10指定 cost 值,Trino 对这个格式支持得很稳定。
如果你用的是 Ambari 管理的 Trino 服务,也可以在 Ambari UI 的 Trino Configs 里找自定义trino-config项,把上面的属性加进去。Ambari 保存后会重新生成配置并触发滚动重启。如果 Trino 是纯手工部署,直接改文件后重启 Trino 服务即可。
3.3 Knox 侧准备事项
在动手改拓扑之前,先确认三件事:
一是 Knox 服务本身的运行状态。HDP 环境下可以执行:
/usr/hdp/current/knox-server/bin/knoxcli.sh status二是 Knox 节点到 Trino 节点的网络连通性。用 Knox 主机直接 curl 一下 Trino:
curl -u trino_service:password http://trino-host.example.com:8080/v1/info如果这一步 401 或者连接超时,先解决网络和认证问题再配 Knox。
三是准备好 Knox 对外使用的认证用户。我这边用的是 LDAP 域账号,Knox 通过 ShiroProvider 接 LDAP 完成认证。如果你不想接 LDAP,也可以配置本地文件 realm,但生产环境建议 LDAP,方便和现有账号体系打通。
4. Knox 拓扑与服务定义:把 Trino UI 挂到网关路径上
4.1 查看 Knox 有没有现成的 Trino/Presto 服务定义
Knox 的 service definition 决定了一个服务角色支持哪些路径、如何做 URL rewrite、使用什么 dispatch 策略。先看本地有没有,避免重复造轮子:
ls /usr/hdp/current/knox-server/conf/services | grep -iE 'trino|presto'我这边 HDP 版本的 Knox 自带了一个presto.xml,但打开后发现它主要覆盖的是/v1/**这一类 REST API 路径,对/ui/**静态资源路径支持不全。直接用它的话,/ui/页面能转发,但页面里的 JS 和 CSS 会加载不出来,或者路径被 rewrite 得乱七八糟。
最简单的办法是自己写一个专门给 Trino UI 用的服务定义。这个文件不影响 Knox 其他服务,安全可靠。
4.2 自定义一个 trino-ui 服务定义
在 Knox 的服务定义目录下新建trino-ui.xml:
sudo vi /etc/knox/conf/services/trino-ui.xml内容如下,核心是两条 rewrite 规则和两个 route:
<service role="TRINO-UI" name="trino-ui" version="1.0.0"> <rewrite name="inbound" path-params="true"> <rule pattern="http://${gateway.host}:${gateway.port}/${gateway.path}/trino-ui/{path=**}?{query}"> <rewrite template="/{path=**}?{query}" /> </rule> </rewrite> <rewrite name="outbound" path-params="true"> <rule pattern="http://trino-host.example.com:8080/{path=**}?{query}"> <rewrite template="http://${gateway.host}:${gateway.port}/${gateway.path}/trino-ui/{path=**}?{query}" /> </rule> </rewrite> <routes> <route path="/**"> <rewrite name="inbound" /> <rewrite name="outbound" /> </route> </routes> </service>这个文件的作用是:
- inbound 规则把用户访问
https://knox-host:8443/gateway/default/trino-ui/xxx的路径剥掉 Knox 网关前缀,转成/xxx。 - outbound 规则把 Trino 返回的响应中所有指向
http://trino-host.example.com:8080的地址改写成 Knox 的地址,这样页面里的链接、重定向、静态资源请求不会跳出网关。
不同 Knox 版本的 rewrite 标签可能有一点点差异,如果加载报错,看 Knox 的 gateway.log 会明确告诉你哪条规则解析失败。大部分网上流传的报错都是路径参数没开path-params="true",导致路径里的参数被吞。
4.3 拓扑文件里注册服务并配置认证
Knox 的拓扑文件放在/etc/knox/conf/topologies/下。生产环境我建议新建一个独立拓扑,比如trino.xml,这样访问路径会更清晰:/gateway/trino/trino-ui/ui/。但如果你们的 Knox 已经有default.xml并且不想引入太多拓扑,也可以在已有 topology 里加 service。
拓扑文件示例:
<topology> <gateway> <provider> <role>authentication</role> <name>ShiroProvider</name> <enabled>true</enabled> <param> <name>sessionTimeout</name> <value>30</value> </param> <param> <name>main.ldapRealm</name> <value>org.apache.knox.gateway.shirorealm.KnoxLdapRealm</value> </param> <param> <name>main.ldapRealm.contextFactory.url</name> <value>ldap://ldap.example.com:389</value> </param> <param> <name>main.ldapRealm.userDnTemplate</name> <value>uid={0},ou=people,dc=example,dc=com</value> </param> <param> <name>urls./**</name> <value>authcBasic</value> </param> </provider> </gateway> <service> <role>TRINO-UI</role> <url>http://trino-host.example.com:8080</url> </service> </topology>注意 provider 里的urls./**设置为authcBasic,表示所有请求都需要 Basic 认证。如果你的 LDAP 用户和 Trino 密码文件里的用户不是同一套,那后面还需要让 Knox 把 Basic header 继续传到后端,我在踩坑部分会详细说。
4.4 重启并观察日志
改完拓扑文件后,重启 Knox 让拓扑生效:
sudo -u knox /usr/hdp/current/knox-server/bin/gateway.sh restart查看启动日志:
tail -f /var/log/knox/gateway.log看到类似Loaded topology trino.xml的日志说明拓扑加载成功。如果出现服务未定义之类的报错,先检查服务定义文件的文件名和 role 名称是否与拓扑里的TRINO-UI匹配。Knox 会根据文件名默认推断 role,文件名不匹配会导致拓扑加载失败。
5. 验证链路:curl 联调、浏览器登录、UI 加载
5.1 先直接验证 Trino HTTP 服务
在配 Knox 时最容易犯的错误是后端 Trino 本身还没就绪就急着调网关。先在 Trino 节点上验证:
curl -u trino_service:password http://trino-host.example.com:8080/v1/info如果返回 JSON 数据,说明双认证配置生效,密码认证可以正常访问。
如果返回 401,查看 Trino 日志里有没有Authentication failed或者Basic authentication failed,确认password.db里的用户名密码是否正确。顺便提一句,http-server.authentication.type如果同时配置KERBEROS和PASSWORD,一定不能写成KERBEROS,PASSWORD带空格,Trino 的属性解析对空格很敏感,写错会导致整个认证配置不生效。
5.2 验证 Knox 代理后的 API
在 Knox 节点或者办公网测试机执行:
curl -k -u alice:password https://knox-host:8443/gateway/trino/trino-ui/v1/info这里的alice是 LDAP 域账号。如果返回和上一步一样的 JSON,说明 Knox 到 Trino 的转发链路已经打通,认证头也被正确传递。
如果这一步报 401,先用-v参数看完整请求响应:
curl -k -v -u alice:password https://knox-host:8443/gateway/trino/trino-ui/v1/info重点看响应头里是否有WWW-Authenticate: Negotiate。如果后端 Trino 仍在要求 Kerberos 而不是 Basic,说明 Knox 转发时把Authorization头丢掉了或者没有带上。
5.3 验证 UI 页面
API 通了之后验证静态页面:
curl -k -u alice:password -i https://knox-host:8443/gateway/trino/trino-ui/ui/正常情况下返回200 OK,Content-Type: text/html。用浏览器的开发者工具打开 Network 面板,随便点击几个 JS 和 CSS 资源,确认它们的请求地址都落在https://knox-host:8443/gateway/trino/trino-ui/前缀下,而不是 Trino 的 8080 地址。
如果前端资源加载不出来,大概率是 outbound rewrite 没有覆盖到静态资源的响应路径。这时不要急着怀疑 Knox,先用 curl 把 HTML 内容拉下来,用文本编辑器搜索trino-host和8080,看看是否还有裸地址残留。
5.4 浏览器实操
上面的步骤都过了之后,浏览器访问:
https://knox-host:8443/gateway/trino/trino-ui/ui/浏览器弹出认证框,输入 LDAP 账号密码,进入 Trino 查询页面。
此时做两件事:先执行一个最简单的SELECT 1,确认 UI 能正常提交查询;再执行一次SHOW SCHEMAS FROM hive,确认 Trino 和 Hive 之间的 Kerberos 票据获取正常。如果查询报GSSException,那是 Trino 引擎自身访问 Hive 或 HDFS 的 Kerberos 认证问题,和 Knox 无关,需要回过去查 Trino 的 keytab 和 catalog 配置。
6. 踩坑记录:那些"明明配对了还是不行"的瞬间
6.1 Knox 自带的 Presto 服务只代理了/v1,UI 404
我一开始图省事,直接用 Knox 自带的PRESTO角色,在拓扑里加了:
<service> <role>PRESTO</role> <url>http://trino-host.example.com:8080</url> </service>结果访问https://knox-host:8443/gateway/trino/presto/ui/返回 404。查 Knox 服务定义目录里的presto.xml,发现 routes 里只有/v1/**的路径映射,UI 路径完全没覆盖。
这就是前面说为什么要自定义trino-ui.xml的原因。Knox 自带的服务定义主要是为了 CLI 和 JDBC 走 REST API 设计的,Web UI 这种带静态资源的场景需要单独定义。
6.2 Trino 把 UI 重定向到自己的 8080
这个坑藏得很深。Trino 的/ui不带尾部斜杠时会 302 到/ui/。在特殊版本下,这个 Location 有时会被拼成http://trino-host.example.com:8080/ui/。浏览器收到后直接跳出了 Knox 网关。
排查方法很简单,用 curl 看 302 响应:
curl -k -u alice:password https://knox-host:8443/gateway/trino/trino-ui/ui看返回的Location字段。如果指向后端 Trino 的 8080 端口,说明 outbound rewrite 没有处理这一个响应头。在我的自定义服务定义里,outbound 规则已经覆盖了 Trino 主机地址,所以这个坑其实是上一个坑的衍生问题,服务定义改好之后自然消失。
6.3 认证头被吞导致 Trino 报 401
Knox 的 ShiroProvider 完成认证后,默认不一定把用户的Authorization头透传给后端。如果 Trino 的 Password Authenticator 收不到 Basic 头,Knox 就会一直向后端发匿名请求,后端返回 401,Knox 再把 401 透给浏览器,浏览器再次弹认证框,形成循环。
我的拓扑里 LDAP 认证的账号和 Trino 密码文件里的账号并不一样。我是这样解决的:让 Knox 认证通过后,在 rewrite 阶段手动注入一个固定的 Basic Authorization 头,无论用户是谁,Knox 后端都统一用trino_service这个服务账号访问 Trino。这样用户侧认证和引擎侧认证解耦,我不用要求每个用户都在 Trino 密码文件里也有账号。
在 Knox 里实现 header 注入,要根据版本选择方式。新版本支持在 provider 里配置header预处理,老版本则需要写一个简单的过滤器。如果你遇到这个问题,可以先查 Knox 的gateway.log,看认证后的请求头长什么样,再决定用哪种方式。这是整个方案里唯一需要动代码或者高级配置的地方,建议留足时间。
6.4 Kerberos 的 SPNEGO 循环:浏览器反复弹框
如果你在 Trino 端只配置了KERBEROS认证,浏览器通过 Knox 访问时会出现反复弹框,输入账号密码也没用。原因很简单:后端返回的是WWW-Authenticate: Negotiate,浏览器以为你要走 Kerberos 协商,但 Knox 又没参与协商,最终双方僵住。
这就是我不推荐 Knox + Trino 纯 Kerberos 方案的原因。只要你能接受 Trino 的 Web UI 走 Password Authenticator,这个问题就不存在。CLI 和 JDBC 依旧走 Kerberos,不会因为开了 PASSWORD 而降低查询链路的安全等级。
6.5 页面能开但查询报 GSSException
Knox 这层完全连通后,Web UI 能打开,执行查询却报GSSException: No valid credentials provided。这个报错来自 Trino 引擎访问 Hive connector 时的 Kerberos 认证,和 Knox 无关。
检查方向:
- Trino 进程是否以
trino用户运行,keytab 是否可以读取。 klist -ekt /etc/trino/conf/trino.keytab确认 principal 和 keytab 条目。jvm.config里有没有设置-Djava.security.krb5.conf=/etc/krb5.conf和-Djavax.security.auth.useSubjectCredsOnly=false。- Trino 的
hive.metastore.authentication.type=KERBEROS和hive.metastore.service.principal是否配置正确。
页面能打开只是前菜,引擎能真正查询才是终点。
6.6 Cookie 和静态资源的 Path 问题
Trino UI 的某些版本会写 cookie,Path默认是/。如果 Knox 后面还挂了其他服务,这个 cookie 可能会污染其他路径。处理方式是在 Knox 的 outbound rewrite 里对Set-Cookie响应头做路径替换。我用的是自定义服务定义,outbound 规则会把Path=/替换成Path=/gateway/trino/trino-ui/,避免影响同域名下的其他服务。
这个坑不一定每个版本都会遇到,但如果页面能打开、查询也能跑,就是刷新后偶尔跳回登录页,先查 cookie 的 Path 对不对。
在我处理这个问题的整个过程中,最核心的体会是一定要把认证链路拆成两段来看:用户到 Knox 是一段,Knox 到 Trino 是另一段。Ambari 开启 Kerberos 只是让第一段的难度变高了,第二段反而可以用更可控的方式来做。如果你们跟我一样不想让 Trino 的 8080 端口暴露在办公网,同时又希望给数据分析师一个不太折腾的网页查询入口,Knox + Trino 双认证这个组合是目前比较稳妥的选择。最后再补一句,生产环境上线前一定要把 Trino 的 8080 端口用防火墙限到只有 Knox 节点能访问,双认证只是功能层面的兜底,网络层才是第一道防线。