揭秘Kubernetes安全配置:为何无需过度担忧allowPrivilegeEscalation

本文深入解析Kubernetes安全上下文中的allowPrivilegeEscalation选项,澄清常见误解,通过实际代码演示其工作原理,并说明何时应该(以及何时不必)禁用它来加强容器安全。

Kubernetes安全上下文允许您在Pod或容器级别配置安全选项。虽然某些参数已被广泛理解,但其他参数可能更加晦涩且令人困惑。在本文中,我们将揭穿关于allowPrivilegeEscalation选项的神话。

TL;DR—allowPrivilegeEscalation是一个安全强化选项,不多不少。如果您能轻松地在您的工作负载上将其关闭以快速获得安全收益,那就这样做吧!否则,它本身并不会导致您被入侵。如果您没有明确禁用它,您很可能也没问题。

什么是‘allowPrivilegeEscalation’?

询问任何安全工程师您的应用程序是否应该被允许“提升权限”,您很可能会收到茫然的眼神、困惑的表情,甚至可能被质疑您的理智。

“提升它们的权限”?

幸运的是,这里存在一个误解。当您问:

如果我不明确将“allowPrivilegeEscalation”标志设置为false,这有关系吗?

……您的安全工程师听到的却是:

如果我不安全的Java应用程序可以逃逸出其容器并在我们的集群中像1999年那样跳舞,这没问题吗?

好消息!你们至少有一个共同点:你们俩都不知道allowPrivilegeEscalation标志是什么意思——说实话,谁能怪你们呢?

关于‘allowPrivilegeEscalation’的常见误解

让我们开门见山:虽然关闭allowPrivilegeEscalation可能有价值,但它是一个安全强化设置,您可以利用它来提高容器化环境的安全性。

具体来说,如果您将allowPrivilegeEscalation保留为true(其默认值):

  • 不会神奇地允许容器内的非特权进程将其权限提升至root。
  • 不会允许容器内运行的进程逃逸容器。
  • 不会允许Pod在集群内执行任何形式的权限提升。

“但是Christophe,”我听到您问,“那它到底能做什么呢?”让我们首先看看它能防止哪种类型的攻击。然后,我们将深入探讨容器运行时如何实现它。

‘allowPrivilegeEscalation’实战

让我们重现一个场景:一个漏洞允许容器内的非特权进程将其权限提升至root。这可能发生在内核级漏洞中,例如DirtyCow、DirtyPipe或OverlayFS中的CVE-2023-0386。我们也可以测试一个更简单(但同样现实)的情况:滥用设置了setuid位的root所属的二进制文件。首先,让我们重现这个场景。然后,我们将看到关闭allowPrivilegeEscalation如何阻止成功利用。

我们将使用以下程序,它使用setreuid(意为“设置真实和有效用户ID”)和setregid来有效地将权限提升至root。根据设计,这仅在二进制文件由root拥有且设置了setuid位时才有效:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void) {
    // 提升至root
    setreuid(0, 0); 
    setregid(0, 0);

    // 生成shell
    char* const argv[] = {"/bin/bash", NULL};
    char* const environ[] = {NULL};
    execve("/bin/bash", argv, environ);
}
1
2
3
gcc escalate.c -Wall -o /tmp/escalate
sudo chown root:root /tmp/escalate
sudo chmod +s /tmp/escalate

我们现在可以使用一个非特权用户来确认这个易受攻击的程序允许我们将权限提升至root:

以下Dockerfile模拟了一个Alpine容器镜像,该镜像以非特权用户身份运行应用程序,并将易受攻击的二进制文件包含在其中:

 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
FROM alpine:3.20 AS builder
WORKDIR /build
RUN cat > escalate.c <<EOF
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>

int main(void) {
    // 提升至root
    setreuid(0, 0); 
    setregid(0, 0);

    // 生成shell
    char* const argv[] = {"/bin/bash", NULL};
    char* const environ[] = {"PATH=/bin:/sbin:/usr/bin:/usr/sbin", NULL};
    if (-1 == execve("/bin/bash", argv, environ)) {
        printf("Unable to execve /bin/bash, errno %d\n", errno);
    }
}
EOF
RUN cat /build/escalate.c
RUN apk add --no-cache gcc musl-dev
RUN gcc escalate.c -Wall -o escalate

FROM alpine:3.20 AS runner
WORKDIR /app
COPY --from=builder /build/escalate ./escalate
RUN chown root:root ./escalate && chmod +s ./escalate
RUN adduser app-user --uid 1000 --system --disabled-password --no-create-home
RUN apk add bash
USER app-user
ENTRYPOINT ["sh", "-c", "echo Application running && sleep infinity"]

让我们构建它并在Kubernetes集群中运行,明确打开allowPrivilegeEscalation(尽管这是默认值):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 构建镜像
docker build . -t my-app:0.1

# 创建kind集群并在其中运行镜像
kind create cluster
kind load docker-image my-app:0.1

kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 1000
  containers:
  - name: my-app
    image: my-app:0.1
    securityContext:
      allowPrivilegeEscalation: true
EOF

正如预期的那样,我们能够利用该漏洞将权限提升至root:

然而,如果我们将allowPrivilegeEscalation设置为false来启动我们的Pod,我们会得到:

发生了什么?对setreuidsetregid的调用失败了。如果我们将错误处理添加到我们的“利用”代码中,错误会变得更明确:

1
2
3
4
5
6
7
// 提升至root
if (setreuid(0, 0) != 0) {
    printf("setreuid(0, 0) failed: %s\n", strerror(errno));
}
if (setregid(0, 0) != 0) {
    printf("setregid(0, 0) failed: %s\n", strerror(errno));
}

‘allowPrivilegeEscalation’如何工作

根据Kubernetes文档:

AllowPrivilegeEscalation控制一个进程是否可以获得比其父进程更多的权限。此布尔值直接控制是否将在容器进程上设置no_new_privs标志。

no_new_privs标志是内核3.5版(2012年发布)中引入的一个内核特性。启用后,它可以确保没有子进程可以获得比其父进程更多的权限。

我们可以通过手动设置no_new_privs然后尝试进行权限提升来确认此行为,使用一个小的实用程序来:

  1. 使用prctl系统调用设置no_new_privs
  2. 创建一个新的sh进程,该进程将对权限提升漏洞“免疫”。

我们需要第二步,因为新设置的标志不会追溯应用于我们已经运行的shell进程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/prctl.h>

int main(void) {
    // 设置no_new_privs
    if (-1 == prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
        printf("Could not set prctl: %s\n", strerror(errno));
    }

    // 生成shell
    char* const argv[] = {"/bin/sh", NULL};
    char* const environ[] = {"PATH=/bin:/sbin:/usr/bin:/usr/sbin", NULL};
    if (-1 == execve("/bin/sh", argv, environ)) {
        printf("Unable to execve /bin/sh, errno %d\n", errno);
    }
}

当我们编译并运行此实用程序时,我们看到它正确地在我们新的shell进程中设置了no_new_privs标志,正如我们可以通过读取/proc/self/status看到的那样:

如果我们现在再次尝试权限提升,请注意它是如何被阻止的——就像我们将allowPrivilegeEscalation设置为false时一样:

这个小操作正是容器运行时在创建新的容器化进程时所做的事情。例如,这是来自runc的容器初始化代码,它被大多数容器运行时使用,例如containerd、CRI-O和Docker:

1
2
3
4
5
6
// 如果NoNewPrivileges为true(由allowPrivilegeEscalation直接控制),则调用prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) [编者注]
if l.config.NoNewPrivileges {
	if err := unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); err != nil {
		return &os.SyscallError{Syscall: "prctl(SET_NO_NEW_PRIVS)", Err: err}
	}
}

您可以看到它执行的过程与我们完全一样:

  1. 检查NoNewPrivileges是否为true(这直接由我们的Kubernetes安全上下文allowPrivilegeEscalation字段控制)
  2. 如果是这种情况,则在创建容器进程之前打开no_new_privs

那么,问题是什么?

安全——就像大多数试图处理系统性故障的学科一样,是关于构建不同层次以确保单个缺陷不会演变成数据泄露。

在此背景下:是的,明确关闭allowPrivilegeEscalation是一种合理且良好的安全强化实践。关闭它可以极大地提高信心,即攻击者在入侵非特权应用程序时无法在容器内将其权限提升至root,从而降低了利用需要root权限的进一步漏洞的风险。

如果您没有在工作负载上关闭它,这是否很糟糕?可能不会。将其视为您尚未启用的(又一个)强化机制。它不会让您被入侵。除非您是一个成熟的安全团队,否则您可能最好首先专注于对您的容器安全路线图有更高价值的项目(有关从威胁角度出发的一些想法,请参阅我的KubeCon EU 2024演讲和文章)。

也就是说,它不是一个您应该忽略的设置;确保它成为您容器安全路线图的一部分。

常见问题解答

‘allowPrivilegeEscalation’的默认值是什么?

默认为true。请参阅相关代码和相关问题,以便在文档中更清楚地说明。

如果我的工作负载在容器内以root身份运行,关闭‘allowPrivilegeEscalation’有什么意义吗?

没有,绝对没有意义。如果您的工作负载以root身份运行,它们在容器内无法实现进一步的权限提升。

如果我的工作负载以“privileged”模式运行或具有CAP_SYS_ADMIN能力,关闭‘allowPrivilegeEscalation’有什么意义吗?

没有意义。事实上,您甚至无法这样做——API服务器将拒绝您的请求(请参阅相关验证代码):

1
The Pod "my-app" is invalid: spec.containers[0].securityContext: Invalid value: cannot set `allowPrivilegeEscalation` to false and `privileged` to true

关闭‘allowPrivilegeEscalation’是否保护免受容器内的所有类型的权限提升?

不是。例如,如果攻击者利用允许他们提升权限的内核缺陷,它将无济于事。也就是说,它应该阻止所有通过利用setuid/setgid进行权限提升的攻击。

‘allowPrivilegeEscalation’和‘privileged’之间有什么联系吗?

没有。关闭allowPrivilegeEscalation是一种安全强化机制。如果您将其保留为默认值,容器内的进程仍然无法轻易提升其权限或逃逸容器。

而启用privileged运行工作负载会使它们像直接在主机上运行的进程一样运行,这使得容器逃逸从设计上变得微不足道。

如果攻击者设法在容器内提升到root权限,难道不是世界末日吗?

又一个误解,有时由驱动安全行业的FUD(恐惧、不确定性和怀疑)愉快地传播。在容器内以root身份运行的进程无法轻易逃逸到容器外。它将不得不利用另一个漏洞或错误配置。

结论

希望本文能更深入地概述‘allowPrivilegeEscalation’是什么、不是什么,以及使用它的明显好处。当我第一次发现它时,我自己也很困惑,并且似乎对许多人来说都是一个困惑的来源,也许是由于其不幸的命名。

感谢您的阅读,让我们在Hacker News、Twitter或Mastodon上继续讨论!

感谢我的同事Rory McCune审阅了这篇文章。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计