最近要采集 yarn 队列使用量,自从集群升级到 HDP 3.1.5 后,访问 yarn resourcemanager 页面需要 kerberos 认证才可访问,这里总结了 3 种方式访问 kerberos 安全页面的方式
curl 用于调试,配合 jq 解析 json 使用
参考 https://docs.cloudera.com/runtime/7.2.10/scaling-namespaces/topics/hdfs-curl-url-http-spnego.html
命令 在执行前,请确认你已经通过 kinit 完成认证1 curl -u : --negotiate "http://rm.example:8088/jmx"
-u :
:使用空用户名和密码进行基本身份验证。在 Kerberos 认证中,实际的身份验证是通过票据而不是用户名和密码完成的,因此这里使用空用户名和密码只是为了满足 curl 的基本身份验证要求。
--negotiate
:启用 GSS-Negotiate 认证,这是 Kerberos 的一种认证机制。
实例 访问 active namenode
注意:-I
, -s
, -v
不影响访问过程-I
用于查看头信息,不看响应内容-v
启用 curl 的详细模式,会显示请求和响应的全部信息,包括请求头、响应头和数据内容。-s
静默模式,不显示进度信息或错误消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 $ curl -I -v -s -u : --negotiate http://<active namenode url>:50070/jmx * About to connect() to <active namenode url> port 50070 (#0) * Trying <active namenode ip>... * Connected to <active namenode url> (<active namenode ip>) port 50070 (#0) > HEAD /jmx HTTP/1.1 > User-Agent: curl/7.29.0 > Host: <active namenode url>:50070 > Accept: */* > < HTTP/1.1 401 Authentication required HTTP/1.1 401 Authentication required < Date: Sat, 05 Aug 2023 10:00:20 GMT Date: Sat, 05 Aug 2023 10:00:20 GMT < Date: Sat, 05 Aug 2023 10:00:20 GMT Date: Sat, 05 Aug 2023 10:00:20 GMT < Pragma: no-cache Pragma: no-cache < X-FRAME-OPTIONS: SAMEORIGIN X-FRAME-OPTIONS: SAMEORIGIN < WWW-Authenticate: Negotiate WWW-Authenticate: Negotiate < Set-Cookie: hadoop.auth=; Path=/; HttpOnly Set-Cookie: hadoop.auth=; Path=/; HttpOnly < Cache-Control: must-revalidate,no-cache,no-store Cache-Control: must-revalidate,no-cache,no-store < Content-Type: text/html;charset=iso-8859-1 Content-Type: text/html;charset=iso-8859-1 < Content-Length: 263 Content-Length: 263 < * Connection #0 to host <active namenode url> left intact * Issue another request to this URL: 'http://<active namenode url>:50070/jmx' * Found bundle for host <active namenode url>: 0x1c6cfa0 * Re-using existing connection! (#0) with host <active namenode url> * Connected to <active namenode url> (<active namenode ip>) port 50070 (#0) * Server auth using GSS-Negotiate with user '' > HEAD /jmx HTTP/1.1 > Authorization: Negotiate <REDACTED> > User-Agent: curl/7.29.0 > Host: <active namenode url>:50070 > Accept: */* > < HTTP/1.1 200 OK HTTP/1.1 200 OK < Date: Sat, 05 Aug 2023 10:00:20 GMT Date: Sat, 05 Aug 2023 10:00:20 GMT < Cache-Control: no-cache Cache-Control: no-cache < Expires: Sat, 05 Aug 2023 10:00:20 GMT Expires: Sat, 05 Aug 2023 10:00:20 GMT < Date: Sat, 05 Aug 2023 10:00:20 GMT Date: Sat, 05 Aug 2023 10:00:20 GMT < Pragma: no-cache Pragma: no-cache < Content-Type: application/json; charset=utf8 Content-Type: application/json; charset=utf8 < X-FRAME-OPTIONS: SAMEORIGIN X-FRAME-OPTIONS: SAMEORIGIN < WWW-Authenticate: Negotiate <REDACTED> WWW-Authenticate: Negotiate <REDACTED> < Set-Cookie: hadoop.auth="u=<username>&p=<principal>&t=kerberos&e=1691265620290&s=<REDACTED>"; Path=/; HttpOnly Set-Cookie: hadoop.auth="u=<username>&p=<pricipal>&t=kerberos&e=1691265620290&s=<REDACTED>"; Path=/; HttpOnly < Access-Control-Allow-Methods: GET Access-Control-Allow-Methods: GET < Access-Control-Allow-Origin: * Access-Control-Allow-Origin: * < Content-Length: 542143 Content-Length: 542143 < * Closing connection 0
过程解释
当你运行这个 curl 命令时,首先它会尝试连接到指定的 URL,并发送一个不包含身份验证信息 的 HTTP 请求。
如果目标 URL 受到 Kerberos 认证保护,服务器会返回一个 HTTP 401 状态码(未授权)的响应,并在头部中包含一个WWW-Authenticate: Negotiate
的字段。这告诉客户端要使用Negotiate 机制来进行身份验证。
客户端接收到这个响应后,会通过 Kerberos 库生成一个 SPNEGO token,这个 token 包含了客户端的身份信息、时间戳、随机数等数据,并使用 Kerberos 的加密机制进行保护。(也就是第二次请求头中 WWW-Authenticate: Negotiate 后面的那很大一串)
SPNEGO 代表 Simple and Protected GSS-API Negotiation Mechanism,你可以理解成 kerberos 在 HTTP 交互认证使用的机制。
如果服务器成功验证了 SPNEGO token,说明客户端的 Kerberos 身份验证通过,服务器将返回 HTTP 200 状态码,表示认证成功。之后,客户端和服务器之间的通信将继续在已认证的状态下进行。
同时会设置一个 Cookie. Set-Cookie: hadoop.auth="u=<username>&p=<principal>&t=kerberos&e=1691265620290&s=<REDACTED>
u 代表 kerberos 用户名 p 代表 kerberos principal t 可能是 type 的意思 s 代表 sign,是一个签名
Golang 使用 https://github.com/jcmturner/gokrb5
我已经在 fork 的 https://github.com/meoww-bot/hadoop_exporter 以及 https://github.com/meoww-bot/hadoop_jmx_exporter 使用此库作为 kerberos 认证的方式
例子 具体可以参考 https://github.com/meoww-bot/hadoop_jmx_exporter/blob/master/lib/krb.go
这里仅简单列出使用 keytab 认证后进行请求的相关代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 kt, err := keytab.Load(ktPath) if err != nil { return nil , fmt.Errorf("failed to load keytab file: %v" , err) } krb5Conf, err := config.Load("/etc/krb5.conf" ) if err != nil { return nil , fmt.Errorf("failed to load Kerberos config: %v" , err) } username, realm := ExtractUsernameAndRealm(principal) if username == "" { return nil , fmt.Errorf("failed to extract username and realm from principal" ) } cli := client.NewClientWithKeytab(username, realm, kt, krb5Conf) err = cli.Login() if err != nil { return nil , fmt.Errorf("failed to login krb5 client" ) } r, err := http.NewRequest("GET" , url, nil ) if err != nil { log.Errorf("could not create request: %v" , err) return nil , fmt.Errorf("could not create request: %v" , err) } fqdn, err := ExtractDomainFromURL(url) if err != nil { log.Errorf("could not extract fqdn from url: %v" , err) return nil , fmt.Errorf("could not extract fqdn from url: %v" , err) } spn := fmt.Sprintf("HTTP/%s" , fqdn) spnegoCl := spnego.NewClient(client, nil , spn) resp, err := spnegoCl.Do(r)
因为在写 https://github.com/meoww-bot/hadoop_jmx_exporter 的时候遇到一个坑,所以去研究了下源码,结果发现请求的原理和 curl 是一样的
spnegoCl.Do(r) 的 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 func (c *Client) Do (req *http.Request) (resp *http.Response, err error) { var body bytes.Buffer if req.Body != nil { teeR := io.TeeReader(req.Body, &body) teeRC := teeReadCloser{teeR, req.Body} req.Body = teeRC } resp, err = c.Client.Do(req) if err != nil { if ue, ok := err.(*url.Error); ok { if e, ok := ue.Err.(redirectErr); ok { e.reqTarget.Header.Del(HTTPHeaderAuthRequest) c.reqs = append (c.reqs, e.reqTarget) if len (c.reqs) >= 10 { return resp, errors.New("stopped after 10 redirects" ) } if req.Body != nil { e.reqTarget.Body = ioutil.NopCloser(&body) } return c.Do(e.reqTarget) } } return resp, err } if respUnauthorizedNegotiate(resp) { err := SetSPNEGOHeader(c.krb5Client, req, c.spn) if err != nil { return resp, err } if req.Body != nil { req.Body = ioutil.NopCloser(&body) } return c.Do(req) } return resp, err }
可以从源码,if respUnauthorizedNegotiate(resp)
当请求是 401 的时候,通过 SetSPNEGOHeader(c.krb5Client, req, c.spn)
设置 SPNEGO 头,然后再次调用方法自身来请求目标。
这里的 SPNEGO 头的 token 实际上是加密后的 Service Ticket,包含用户的身份信息和对服务的权限。也就是说,你,啊,虽然是已经认证了的用户,但是 HTTP 服务端并不知道你的权限是什么样的,你得先找 TGS 拿一张 Service Ticket 给 HTTP 服务端,HTTP 服务端才让你访问。
python 项目组前运维大佬采集 yarn resourcemanager 用量用的是 python 写的,因为内网安装 python 库比较麻烦,这个版本我没有再继续维护,转而使用 go 版本了。
在初次看到这份代码之前还是很好奇的,毕竟当时认为 kerberos 是个很复杂的东西。
使用了requests_kerberos
的 HTTPKerberosAuth
精简代码如下1 2 3 4 5 6 7 8 9 10 11 12 13 14 from requests_kerberos import HTTPKerberosAuthimport requestsimport oskeytabfile="/path/to/user.keytab" pricipal="[email protected] " shell_cmd = 'kinit -kt %s %s' % (keytabfile, principal)os.system(shell_cmd) krb5auth = HTTPKerberosAuth(hostname_override=fqdn, principal=principal) r = requests.get(active_nn_url, auth=krb5auth) ....