共计 7115 个字符,预计需要花费 18 分钟才能阅读完成。
逛社区
注:这一篇文章收回来后,又用 GPT-4 重写了一次代码,详情见《给 Helm 修复一个 Bug – 每天 5 分钟玩转 GPT 编程系列(2)》。为了独立成文,两篇文章都加了一些结尾结尾,所以局部反复。本文更加侧翻阅源码重定位 Bug 的过程,隔壁那篇偏重用 GPT-4 写代码的过程。
酒足饭饱,闲来无事,关上 Helm 我的项目的 issues 列表,咦,这个 issue 看起来眉清目秀呀:
其实一点都不“眉清目秀”,这段 issue 形容太简短了,没有任何排版,看起来很累。咱就看关键词吧:helm upgrade,sensitive data,error,好的,这就够了,加在一起就是说 当 helm upgrade 命令执行失败的时候,可能回导致敏感数据泄露。
盘它!
领工作
只管曾经有人留言认领这个工作,不过看形容仿佛他并不会马上开始编码,而是心愿进一步沟通计划。好办好办,转给我吧:
倒是挺罗唆,分分钟就 assign 给我了。
不过,其实,这会我并不知道怎么修复这个 bug,我也不相熟 Helm 的源码,外加曾经小半年没咋写过 Golang 了,其实心里挺慌。试试看吧,到底是宝刀未老还是老骥伏枥。
复现 Bug
要修复 Bug,第一步天然是先看能不能稳固复现。外加 issue 形容里其实我看不出来如何复现,也不是太看清楚问题景象是什么,总之,复现问题是事不宜迟。
用 GPT-4 来筹备 Bug 复现资料
- 我的需要
上面就是想想本人须要啥资料,而后抛给 GPT。或者我能够这样说:
我发现 Helm 在执行 upgrade 的时候,如果出错,会导致 Secret 里的数据被明文打印进去,存在敏感数据泄露危险。当初我想复现这个问题,我构想的步骤是这样的:1. 结构一个蕴含 Secret 的失常的 Helm Chart,Secret 里的数据是从 values.yaml 里读取的,values.yaml 中间接应用 base64 编码字符串,而不是在 secret.yaml 中应用 b64enc;2. 而后执行 helm install,此刻应该没有问题异样;3. 批改 values.yaml 中的 base64 字符串,让其非法(比方删除一个结尾的“=”);4. 执行 helm upgrade 命令,看后果是不是存在敏感数据泄露。请通知我相干命令和操作等。
- 将需要通知 DevChat
- DevChat 给出的残缺答复
首先,我会概括你的需要:
- 你想要复现一个问题,该问题是在应用 Helm 进行 upgrade 操作时,如果出错,会导致 Secret 中的数据被明文打印进去,存在敏感数据泄露危险。
- 你曾经构想了一些步骤,包含创立一个蕴含 Secret 的 Helm Chart,而后进行一些操作来触发这个问题。
上面是我为你筹备的相干命令和操作:
- 创立一个蕴含 Secret 的 Helm Chart。首先,你须要创立一个新的 Helm Chart,能够应用以下命令:
helm create mychart
而后,你须要在 mychart/templates 目录下创立一个新的文件 secret.yaml,并增加以下内容:
apiVersion: v1 kind: Secret metadata: name: mysecret type: Opaque data: mykey: {{.Values.mysecret.mykey}}
在 mychart/values.yaml 文件中,增加以下内容:
mysecret: mykey: "SGVsbG8gd29ybGQ=" # "Hello world" 的 base64 编码
- 执行 helm install 命令。在你的 Helm Chart 目录(即 mychart)下,执行以下命令:
helm install myrelease .
此时,应该没有任何问题或异样。
- 批改 values.yaml 文件中的 base64 字符串,让其非法。你能够将 values.yaml 文件中的 mykey 值批改为一个非法的 base64 字符串,例如删除一个结尾的“=”:
mysecret: mykey: "SGVsbG8gd29ybGQ" # 非法的 base64 字符串
- 执行 helm upgrade 命令。在你的 Helm Chart 目录下,执行以下命令:
helm upgrade myrelease .
此时,你应该能看到错误信息,包含你的 Secret 数据。
以上就是复现你所说问题的步骤。如果你在执行这些步骤时遇到任何问题,或者须要更多的帮忙,欢送随时向我发问。
对着 GPT-4 给的命令复现 Bug
DevChat 诚不我欺啊,一顿复制粘贴敲键盘,最初我看到了这个:
留神这个谬误日志里的这部分:
Error: UPGRADE FAILED: cannot patch "mysecret" with kind Secret: ""is invalid: patch: Invalid value:"{\"apiVersion\":\"v1\",\"data\":{\"mykey\":\"SGVsbG8gd29ybGQ\"}……
这里的 mykey: SGVsbG8gd29ybGQ 看起来仿佛不太敏感,不过它其实意味着如果你存了 10 个明码在 Secret 里,当更新的时候写错了一个,其余正确的 9 个会被无差别打印进去。这个日志里蕴含的是一个未脱敏的残缺的 Secret 资源实例内容。
定位 Bug
从日志里来看,JSON 局部大概率是 K8s 相干的库返回的内容,Helm 里间接拼接到本人日志里打印进去了。咱们须要找到最靠近这个“K8s 相干调用”的中央,而后找到这串日志怎么来的,再加一层“脱敏”。
- 搜寻关键字“UPGRADE FAILED”
太侥幸了,只有一个后果(显著是 upgrade.go 这里),那就长刀直入吧:
rel, err := client.RunWithContext(ctx, args[0], ch, vals)
if err != nil {return errors.Wrap(err, "UPGRADE FAILED")
}
- 持续跟
RunWithContext()
办法里哪里返回了谬误日志
进去 RunWithContext()
办法,根本从名字上就能判断是“执行降级过程”的时候出的错,因为一开始的谬误日志里有一句“cannot patch “mysecret” with kind Secret”,这显然也不是 Helm 自身的行为,而是某个 K8s 相干的库返回的,因而必定是开始执行的时候出的错,也就是 performUpgrade()
这个办法返回的 err 里蕴含了咱们所寻找的谬误日志。
func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
//……
res, err := u.performUpgrade(ctx, currentRelease, upgradedRelease)
if err != nil {return res, err}
//……
return res, nil
}
- 持续跟
performUpgrade()
这个办法有点长,认真看看,大概率是在结尾的时候 result.e
里蕴含了咱们寻找的 err,而这个 result 是一个 Channel,在 releasingUpgrade 这个协程里实现了数据写入。行,下一步,持续看 releasingUpgrade()
func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedRelease *release.Release) (*release.Release, error) {
// ……
go u.releasingUpgrade(rChan, upgradedRelease, current, target, originalRelease)
go u.handleContext(ctx, doneChan, ctxChan, upgradedRelease)
select {
case result := <-rChan:
return result.r, result.e
case result := <-ctxChan:
return result.r, result.e
}
- 持续看
releasingUpgrade()
到这里就差不多了,这里有要害的一行代码:u.cfg.KubeClient.Update(current, target, u.Force)
:
func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *release.Release, current kube.ResourceList, target kube.ResourceList, originalRelease *release.Release) {
// ……
results, err := u.cfg.KubeClient.Update(current, target, u.Force)
if err != nil {u.cfg.recordRelease(originalRelease)
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
return
}
//……
}
到这里就能猜到很快就应该靠近 Helm 和 K8s 相干库的接壤点了,“KubeClient”这个名字看起来就不像是 Helm 本身逻辑里的代码。咱们持续来看 Update 就行:
- 持续跟
Update()
接口
这里比拟直观,咱们要找到的实现必定是 client.go
外面那个 Update。
- 看
Update()
接口的实现
这个办法看起来也挺简单,外面藏了一个要害的 updateResource()
函数:
func (c *Client) Update(original, target ResourceList, force bool) (*Result, error) {
// ……
err := target.Visit(func(info *resource.Info, err error) error {
//……
if err := updateResource(c, info, originalInfo.Object, force); err != nil {c.Log("error updating the resource %q:\n\t %v", info.Name, err)
updateErrors = append(updateErrors, err.Error())
}
// ……
})
// ……
}
- 持续看
updateResource()
函数
func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, force bool) error {
// ……
if force {// ……} else {
// ……
// send patch to server
c.Log("Patch %s %q in namespace %s", kind, target.Name, target.Namespace)
obj, err = helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
if err != nil {return errors.Wrapf(err, "cannot patch %q with kind %s", target.Name, kind)
}
}
// ……
}
到这里就比拟明确了,是 helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
这个办法调用返回了一个蕴含敏感数据的 err。如果持续看一眼 Patch 办法的定义,就能找到:
这是 k8s.io/cli-runtime
里的代码,曾经来到了 Helm 的“管辖范畴”。
修复思路
很显著,敏感数据来自于这几行代码:
obj, err = helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
if err != nil {return errors.Wrapf(err, "cannot patch %q with kind %s", target.Name, kind)
}
咱们心愿尽脱敏这些数据,天然是在以后函数 / 办法内实现这个过程,不能把锅留给调用方(下层函数)。所以下一步就是在 if err != nil {
之后,return
之前退出一个日志过滤函数,在这个函数内实现日志脱敏。
编写代码
- 减少一个函数来实现脱敏逻辑:
// desensitizeLog replaces the data in a Secret with {"key": "***"}.
// e.g. "data": {"username": "admin", "password": "password"} becomes "data": {"username": "***", "password": "***"}
func desensitizeLog(errLog string) string {
// Find the start and end index of the JSON string
start := strings.Index(errLog, "{\\\"apiVersion")
end := strings.LastIndex(errLog, ",\\\"kind\\\":\\\"Secret\\\"")
// Extract the JSON string and unescape it
jsonStr := strings.ReplaceAll(errLog[start:end], "\\\"", "\"")+ "}"
// Unmarshal the JSON string into a Secret struct
var secret Secret
json.Unmarshal([]byte(jsonStr), &secret)
// Desensitize the data in the Secret struct
for key := range secret.Data {secret.Data[key] = "***"
}
// Marshal the Secret struct back into a JSON string
desensitizedJson, _ := json.Marshal(secret)
// Escape the JSON string and replace it in the original log
fixedDesensitizedJson := strings.ReplaceAll(string(desensitizedJson), "\"", "\\\"")
desensitizedLog := errLog[:start] + fixedDesensitizedJson[:len(fixedDesensitizedJson)-1] + errLog[end:]
return desensitizedLog
}
- 把函数调用加到老代码里:
c.Log("Patch %s %q in namespace %s", kind, target.Name, target.Namespace)
obj, err = helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
if err != nil {sanitizeLog:=err.Error()
if kind == "Secret" {sanitizeLog = desensitizeLog(err.Error())
}
return errors.Wrapf(errors.New(sanitizeLog), "cannot patch %q with kind %s", target.Name, kind)
}
当 kind 为 Secret 的时候,就走一遍脱敏过程。
- 再加一段 UT 吧:
func TestDesensitizeLog(t *testing.T) {
// Define a test error log
errLog := `cannot patch "test-release-secret" with kind Secret: "" is invalid:
patch: Invalid value: "{\"apiVersion\":\"v1\",\"data\":{\"secretKey\":\"hello\", \"anotherKey\":\"world\"},\"kind\":\"Secret\",`
// Call the function
result := desensitizeLog(errLog)
// Check if the "data" field has been correctly sanitized
if !strings.Contains(result, "\\\"secretKey\\\":\\\"***\\\"") ||
!strings.Contains(result, "\\\"anotherKey\\\":\\\"***\\\"") {t.Errorf("The function did not correctly sanitize the error log")
}
}
测试
UT 须要能通过,这就不必赘述了。UT 过了之后,手动测试下:
酷,看起来挺谐和了。
提 PR
最初一步,提个 PR:
- https://github.com/helm/helm/pull/12183
对于如何参加开源我的项目,如何在 GitHub 上提 PR,我这有一篇十分具体的文章:
- 如何参加开源我的项目 – 细说 GitHub 上的 PR 全过程
ok,不啰嗦了。可能你曾经发现,结尾有点仓促,没错,我写到一半的时候,到饭点了,连忙收个尾,填肚子去咯~