最近要采集 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

过程解释

  1. 当你运行这个 curl 命令时,首先它会尝试连接到指定的 URL,并发送一个不包含身份验证信息的 HTTP 请求。

  2. 如果目标 URL 受到 Kerberos 认证保护,服务器会返回一个 HTTP 401 状态码(未授权)的响应,并在头部中包含一个WWW-Authenticate: Negotiate的字段。这告诉客户端要使用Negotiate 机制来进行身份验证。

  3. 客户端接收到这个响应后,会通过 Kerberos 库生成一个 SPNEGO token,这个 token 包含了客户端的身份信息、时间戳、随机数等数据,并使用 Kerberos 的加密机制进行保护。(也就是第二次请求头中 WWW-Authenticate: Negotiate 后面的那很大一串)

SPNEGO 代表 Simple and Protected GSS-API Negotiation Mechanism,你可以理解成 kerberos 在 HTTP 交互认证使用的机制。

  1. 如果服务器成功验证了 SPNEGO token,说明客户端的 Kerberos 身份验证通过,服务器将返回 HTTP 200 状态码,表示认证成功。之后,客户端和服务器之间的通信将继续在已认证的状态下进行。

  2. 同时会设置一个 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
// 读取 keytab
kt, err := keytab.Load(ktPath)
if err != nil {
return nil, fmt.Errorf("failed to load keytab file: %v", err)
}

// 读取 krb5 配置文件
krb5Conf, err := config.Load("/etc/krb5.conf")
if err != nil {
return nil, fmt.Errorf("failed to load Kerberos config: %v", err)
}

// 从 pricipal 提取 username 和 realm
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)

// 登陆 client,获取到已经认证的 client
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)

}

// 从 url 提取 fqdn 域名
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)

}

// 生成 spenego 服务 principal
spn := fmt.Sprintf("HTTP/%s", fqdn)

// 从 client 获取 spnego client
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
// Do is the SPNEGO enabled HTTP client's equivalent of the http.Client's Do method.
func (c *Client) Do(req *http.Request) (resp *http.Response, err error) {
var body bytes.Buffer
if req.Body != nil {
// Use a tee reader to capture any body sent in case we have to replay it again
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 {
// Picked up a redirect
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 {
// Refresh the body reader so the body can be sent again
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 {
// Refresh the body reader so the body can be sent again
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_kerberosHTTPKerberosAuth

精简代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from requests_kerberos import HTTPKerberosAuth
import requests
import os

keytabfile="/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)

....