问题形容
近期将 jenkins 从低版本升级到最新的 2.312 版本之后,对 jenkins 发动的 API 调用,均呈现 403
谬误。
curl -s -XPOST http://127.0.0.1:8080/credentials/store/system/domain/_/createCredentials \
> --user admin:pass@word1 --data-urlencode 'json={
quote> "":"0",
quote> "credentials": {
quote> "scope": "GLOBAL",
quote> "id": "credential_id_here",
quote> "username": "username_here",
quote> "password": "password_here",
quote> "description": "My new credentials",
quote> "$class": "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl"
quote> }
quote> }'
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Error 403 No valid crumb was included in the request</title>
</head>
<body><h2>HTTP ERROR 403 No valid crumb was included in the request</h2>
<table>
<tr><th>URI:</th><td>/credentials/store/system/domain/_/createCredentials</td></tr>
<tr><th>STATUS:</th><td>403</td></tr>
<tr><th>MESSAGE:</th><td>No valid crumb was included in the request</td></tr>
<tr><th>SERVLET:</th><td>Stapler</td></tr>
</table>
<hr><a href="https://eclipse.org/jetty">Powered by Jetty:// 9.4.43.v20210629</a><hr/>
</body>
</html>
依据网上查找的材料显示,是因为 jenkins 开启了 CSRF Protection
, 其实低版本也有这个性能,只是可能不欠缺,或者没有限度的这么死,如下图所示:
上图中左侧为降级前的旧版本 2.183
,右侧为新版本 2.312
,能够看到旧版本容许敞开跨域爱护,新版本不容许。通过一轮测试和钻研,发现的确无奈敞开。
仔细阅读 jenkins 文档,发现 CSRF 有以下阐明:
https://www.jenkins.io/doc/bo…
The Default Crumb Issuer encodes the following information in the hash used as crumb:
- The user name that the crumb was generated for
- The web session ID that the crumb was generated in
- The IP address of the user that the crumb was generated for
- A salt) unique to this Jenkins instance
All of this information needs to match when a crumb is sent back to Jenkins for that submission to be considered valid.
The only supported option Enable proxy compatibility removes information about the user IP address from the token. This can be useful when Jenkins is running behind a reverse proxy and a user’s IP address as seen from Jenkins would regularly change.
大略意思是说,默认启用的The *Default Crumb Issuer
, 这个拦截器会计算传递的 crumb 值的 hash 是否是可用 hash, 这个 hash 的起源是通过用户名、sessionID, 申请 IP,以及拜访的 jenkins 的惟一标识生成的。
其中惟一能够用的配置项 Enable proxy compatibility
, 只凋谢 IP 的校验,这能够应用在应用网络代理的场景中。
如何解决
依据这份文档,看起来对 jenkins 的调用,必须要先申请一个 crumb 的 hash 值,在每次申请的时候传递给它,例如:
JENKINS_CRUMB=$(curl -s 'http://127.0.0.1:8080/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)' --user admin:pass@word1)
curl -s -XPOST -H "Jenkins-Crumb:8dbf1060a4fd7a0120da0dd1a678cf82149dcd45d868c7799b760e79132fb774" http://127.0.0.1:8080/credentials/store/system/domain/_/createCredentials \
--user admin:pass@word1 --data-urlencode 'json={"": "0",
"credentials": {
"scope": "GLOBAL",
"id": "credential_id_here",
"username": "username_here",
"password": "password_here",
"description": "My new credentials",
"$class": "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl"
}
}'
依据下面这个网上找到的案例,发现运行之后还是会产生 403,其中 JENKINS_CRUMB
确定是有值的,见鬼了。
文档欠缺的时候就是这样无语,消耗了工夫查找网上可能的案例时,终于发现一个更多的信息:
看起来还须要多传递一个 cokkie 的信息,不论有没有用,先测试再说,没想到真的胜利了,jenkins 官网为什么不给一个例子呢,吐槽 ….
无效测试如下:
curl -verbose -s 'http://127.0.0.1:8080/crumbIssuer/api/json' --user admin:pass@word1
* About to connect() to 127.0.0.1 port 8080 (#0)
* Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
* Server auth using Basic with user 'admin'
> GET /crumbIssuer/api/json HTTP/1.1
> Authorization: Basic YWRtaW46cGFzc0B3b3JkMQ==
> User-Agent: curl/7.29.0
> Host: 127.0.0.1:8080
> Accept: */*
> Referer: rbose
>
< HTTP/1.1 200 OK
< Date: Sat, 18 Sep 2021 06:48:15 GMT
< X-Content-Type-Options: nosniff
< X-Jenkins: 2.312
< X-Jenkins-Session: 4af5e654
< X-Frame-Options: deny
< Content-Type: application/json;charset=utf-8
< Set-Cookie: JSESSIONID.443fa9b8=node08lm1pknhy9zr1nr1gd79rdpd725.node0; Path=/; HttpOnly # 留神 JSESSIONID
< Expires: Thu, 01 Jan 1970 00:00:00 GMT
< Content-Length: 163
< Server: Jetty(9.4.43.v20210629)
<
* Connection #0 to host 127.0.0.1 left intact
{"_class":"hudson.security.csrf.DefaultCrumbIssuer","crumb":"7f0ad0185a3a25c101ef654af337de01b822f2c4220a819ce1daaae7c61da059","crumbRequestField":"Jenkins-Crumb"}#
~ curl -s -XPOST --cookie "JSESSIONID.443fa9b8=node08lm1pknhy9zr1nr1gd79rdpd725.node0" -H "Jenkins-Crumb:7f0ad0185a3a25c101ef654af337de01b822f2c4220a819ce1daaae7c61da059" \
http://127.0.0.1:8080/credentials/store/system/domain/_/createCredentials \
--user admin:pass@word1 --data-urlencode 'json={"": "0",
"credentials": {
"scope": "GLOBAL",
"id": "credential_id_here",
"username": "测试 crumb",
"password": "password_here",
"description": "My new credentials",
"$class": "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl"
}
}'
如上例子所示,须要再传递的时候减少 cokkie 和 header,传递 JSESSIONID 和 Jenkins-Crumb,以及账号密码或者 token. 那么程序发动调用之前,还须要先申请 session 和 crumb,并放弃 session,如果 session 过期,还会呈现 403, 其中少不了容错解决的局部。