共计 8596 个字符,预计需要花费 22 分钟才能阅读完成。
从 .NET 8 起,咱们所有的 Linux 容镜像都将蕴含一个 non-root 用户。只须要一行代码就能以 non-root 用户身份托管您的 .NET 容器。这个平台级的变动将会使您的应用程序更加平安,并使 .NET 成为最平安的开发者生态系统之一。这是一个小的变动,但对深层进攻 (defense in depth) 影响微小。
这一变动的灵感来源于咱们晚期在 Ubuntu Chiseled 容器中启用 .NET 的我的项目。Chiseled(又称 “distroless”)镜像旨在像设施一样,因而 non-root 是这些镜像最简略的设计抉择。咱们意识到,咱们能够将 Chiseled 容器的 non-root 性能利用于咱们公布的所有容器镜像。通过这样做,咱们进步了 .NET 容器镜像的平安规范。
这篇文章是对于 non-root 容器的益处,创立它们的工作流程以及工作原理。在后续的文章中,咱们也将探讨如何在 Kubernetes 中更好地应用这些镜像。另外,如果想要更简略的选项,可查看 .NET SDK 的内置容器反对。
最小特权
将容器托管为 non-root 合乎最小特权准则。这是由操作系统提供的收费保安。如果以 root 身份运行利用,那利用过程能够在容器中执行任何操作,例如批改文件、安装包或者运行任意可执行文件。如果您的应用程序受到攻打,这将是一个隐患。然而如果以 non-root 身份运行利用,您的利用过程将无奈执行太多操作,从而极大地限度了攻击者的歹意操作。
non-root 容器也能够认为是对平安供应链的奉献。通常,人们都是从阻止不良依赖项更新或排查组件起源的角度来探讨平安供应链。non-root 容器在这两者之后。如果在您的过程中呈现了不良依赖项(很有可能遇到这种状况),那么 non-root 容器可能是最好的最初防线。Kubernetes hardening 最佳做法要求以 non-root 用户运行容器,也是出于这个起因。
浅识 app
咱们所有的 Linux 镜像从 .NET 8 开始将蕴含一个 app 用户。app 用户将可能运行您的应用程序,但不能删除或更改容器镜像中的任何文件(除非您明确容许这样做)。这个命名也是高深莫测,app 用户除了运行您的应用程序外,简直不能做任何事件。
这个 app 用户实际上并不是新的。它和咱们用于 Ubuntu Chiseled 镜像的那个程序是一样的。这是个要害的设计点。从 .NET 8 开始,咱们所有的 Linux 容器镜像都将蕴含 app 用户。这意味着您能够在咱们提供的镜像之间进行切换,并且 user 和 uid 是一样的。
接下来我将形容 docker CLI 的全新体验。
$ docker run --rm mcr.microsoft.com/dotnet/aspnet:8.0-preview cat /etc/passwd | tail -n 1
app:x:64198:64198::/home/app:/bin/sh
这是镜像中 /etc/passwd file 的最初一行。这是 Linux 用于治理用户的文件。
依据行业领导咱们抉择了一个绝对较高的 uid,靠近 2^16。咱们还决定此用户该当有一个主目录。
$ docker run --rm -u app mcr.microsoft.com/dotnet/aspnet:8.0-preview bash -c "cd && pwd"
/home/app
咱们看了一下,发现 Node.js,Ubuntu 23.04+ 和 Chainguard 都在同一个打算中。Nice!
$ docker run --rm node cat /etc/passwd | tail -n 1
node:x:1000:1000::/home/node:/bin/bash
$ docker run ubuntu:lunar cat /etc/passwd | tail -n 1
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
$ cat out/layers/ruby/etc/passwd | tail -n 1
nonroot:x:65532:65532:Account created by apko:/home/nonroot:/bin/sh
最初一个是护链镜像(Chainguard)。这些镜像的构造不同,因而应用了不同的模式。每个人都能够创立本人的用户。要害是防止重叠,尤其是 UIDs 重叠。
容器镜像中有很多用户,但没有一个实用于这个用例。缩小用户的数量诚然很好,但不太可能,这也是应用 distroless/Chiseled 镜像的益处之一。
Windows 容器曾经具备了 non-admin 性能,和 ContainerUser 用户。咱们抉择不增加 app 到 Windows 容器镜像。您应该遵循 Windows 团队对于如何最好地爱护 Windows 容器镜像的领导。
应用 app
“Non-root-capable”:应用单行 USER 指令将您的容器配置为 non-root 用户。
Docker 和 Kubernetes 能够轻松指定要用于容器的用户。这是一个单行指令。依据咱们的定义,“non-root-capable”意味着您能够应用一行指令切换到 non-root。这是十分弱小的,因为单行指令的易用性打消了任何不平安运行的理由。
留神:aspnetapp 从头至尾用作您的 app 的替代品。
您能够应用 通过 CLI 设置用户
-u
$ docker run --rm -u app mcr.microsoft.com/dotnet/runtime-deps:8.0-preview whoami
app
通过 CLI 指定用户很好,但更多的是用于测试或诊断计划。生产 apps 时最好在 Dockerfile 中应用 username 或 uid 定义 USER。
作为 user:USER app
作为 UID:USER 64198
咱们正在为 UID 增加环境变量。这将启用以下模式。USER $APP_UID
咱们认为这种模式是最好的做法,因为它能够分明地表明您正在应用哪个用户,防止反复的魔数(magic numbers),并且应用 UID,如果您应用的是 Kubernetes,所有这些都能够很好地工作。
如果您不执行任何的操作,那所有都将跟之前一样,您的镜像将持续以 root 身份运行。咱们心愿您采取额定的步骤,以 app 用户身份运行您的容器。您可能想晓得为什么咱们默认状况下没有切换到 non-root 用户。
切换到端口 8080
这个我的项目最大的症结是咱们裸露的端口。事实上,这是一个十分辣手的问题,以至于咱们不得不做出重大扭转。
咱们决定对今后所有容器镜像的端口进行标准化。这个决定是基于咱们晚期应用 Chiseled 镜像的教训,它曾经在端口 8080 上侦听,当初所有的图像都匹配了。
然而,ASP.NET core 利用(应用咱们的 .NET 7 和更早版本的容器镜像)侦听端口 80。问题是端口 80 是一个须要权限的特权端口(至多在某些中央)。实质上这与 non-root 容器不兼容。
您能够在咱们的镜像中看到端口的配置形式。
对于 .NET 8:
$ docker run --rm mcr.microsoft.com/dotnet/aspnet:8.0-preview bash -c "export | grep ASPNETCORE"
declare -x ASPNETCORE_HTTP_PORTS="8080"
对于 .NET 7 及更早版本:
$ docker run --rm mcr.microsoft.com/dotnet/aspnet:7.0 bash -c "export | grep ASPNETCORE"
declare -x ASPNETCORE_URLS="http://+:80"
接下来,您将须要更改端口映射。
您能够通过 CLI 来执行此操作。您须要在映射的左边设置 8080。右边的能够匹配,也能够是另一个值。
docker run --rm -it -p 8080:8080 aspnetapp
一些用户可能心愿持续应用端口 80 和 root。没问题,您依然能够这样做。
您能够在 Dockerfile 中或通过 CLI 从新定义 ASPNETCORE_HTTP_PORTS。
对于 Dockerfile:ENV ASPNETCORE_HTTP_PORTS=80
对于 Docker CLI:
docker run --rm -e ASPNETCORE_HTTP_PORTS=80 -p 8000:80 aspnetapp
.NET 8 Windows 容器镜像也应用端口 8080。
>docker run --rm mcr.microsoft.com/dotnet/aspnet:8.0-preview-nanoserver-ltsc2022 cmd /c "set | findstr ASPNETCORE"
ASPNETCORE_HTTP_PORTS=8080
ASPNETCORE_HTTP_PORTS 是一个新的环境变量,用于指定 ASP.NET core(实际上是 Kestrel)要侦听的端口(或多个端口)。它采纳一个以分号分隔的端口值的列表。.NET 8 图像应用这个新的环境变量,而不是 ASPNETCORE_URLS(在 .NET 6 和 7 图像中应用)。ASPNETCORE_URLS 依然是一个有用的高级性能。它能够在一个配置中同时指定原始的 HTTP 和 TLS 端口,并笼罩 ASPNETCORE_HTTP_PORTS 和 ASPNETCORE_HTTPS_PORTS。
Non-root 使用
让咱们从几个不同的角度看一下 non-root 是什么样子的,这样您就能够更好地理解理论状况。我在 WSL22 中应用 Ubuntu 10.2。
咱们增加了一个 Dockerfile,以便您能够亲自尝试这个计划。它将容器配置为始终以 app 运行。它应用的是咱们的 aspnetapp 示例。
$ pwd
/home/rich/git/dotnet-docker/samples/aspnetapp
$ cat Dockerfile.alpine-non-root | tail -n 2
USER app
ENTRYPOINT ["./aspnetapp"]
$ docker build --pull -t aspnetapp -f Dockerfile.alpine-non-root .
让咱们看看咱们是否能够察看用户的行为,该用户曾经在 Dockerfile 中进行了设置。
$ docker run --rm -d -p 8000:8080 aspnetapp
5bde77feebdf76ff370815f41a8989a880d51a4037c91e2ac8c6f2c269b759ad
$ curl http://localhost:8000/Environment
{"runtimeVersion":".NET 8.0.0-preview.2.23128.3","osVersion":"Linux 5.15.90.1-microsoft-standard-WSL2 #1 SMP Fri Jan 27 02:56:13 UTC 2023","osArchitecture":"X64","user":"app","processorCount":16,"totalAvailableMemoryBytes":67429986304,"memoryLimit":9223372036854771712,"memoryUsage":30220288}
$ docker exec 5bde77feebdf76ff370815f41a8989a880d51a4037c91e2ac8c6f2c269b759ad ls -l
total 188
-rw-r--r-- 1 root root 127 Jan 20 17:14 appsettings.Development.json
-rw-r--r-- 1 root root 151 Oct 19 21:59 appsettings.json
-rwxr-xr-x 1 root root 78320 Mar 16 16:51 aspnetapp
-rw-r--r-- 1 root root 463 Mar 16 16:51 aspnetapp.deps.json
-rw-r--r-- 1 root root 51200 Mar 16 16:51 aspnetapp.dll
-rw-r--r-- 1 root root 35316 Mar 16 16:51 aspnetapp.pdb
-rw-r--r-- 1 root root 469 Mar 16 16:51 aspnetapp.runtimeconfig.json
drwxr-xr-x 5 root root 4096 Mar 16 16:51 wwwroot
$ docker exec 5bde77feebdf76ff370815f41a8989a880d51a4037c91e2ac8c6f2c269b759ad ps
PID USER TIME COMMAND
1 app 0:00 ./aspnetapp
53 app 0:00 ps
请留神从下面的环境端点返回的 JSON 内容中的用户属性。
您能够看到该应用程序是以 app 运行的,并且文件归 root 所有。这意味着应用程序文件正在受到爱护,不会被此用户更改。这种拆散是咱们持续以 root 身份公布镜像的起因之一。如果咱们以 app 公布他们,那么默认状况下您的利用二进制文件将不会受到 app 用户的爱护。如果咱们以 app 公布镜像,您依然能够实现这种拆散,但您的 Docker 文件(也包含咱们的)将会因为大量的用户切换而变得凌乱,这没有任何益处。
在咱们看来,根底镜像制作者应该齐全以 root 身份公布平台镜像。这是惟一好的通用模型。
应用程序的二进制文件是由 root 领有的,因为它们是由构建 /SDK 阶段产生的,而这个阶段是以 root 用户身份运行的。这并不是因为在 Docker 文件中最初的 COPY 之后,用户被扭转为 app。请留神,COPY 有语义。
参考 Dockerfile:
所有新的文件和目录都是以 UID 和 GID 为 0 创立的,除非可选的 -chown 标记指定一个给定的用户名、组名或 UID/GID 组合,以要求对复制的内容领有特定的所有权。
让咱们在这个容器上尝试一些 rootful 操作,在同一个容器上应用 docker exec。
$ docker exec 5bde77feebdf76ff370815f41a8989a880d51a4037c91e2ac8c6f2c269b759ad rm aspnetapp.pdb
rm: can't remove'aspnetapp.pdb': Permission denied
$ docker exec 5bde77feebdf76ff370815f41a8989a880d51a4037c91e2ac8c6f2c269b759ad touch /file
touch: /file: Permission denied
$ docker exec 5bde77feebdf76ff370815f41a8989a880d51a4037c91e2ac8c6f2c269b759ad which dotnet
/usr/bin/dotnet
$ docker exec 5bde77feebdf76ff370815f41a8989a880d51a4037c91e2ac8c6f2c269b759ad rm /usr/bin/dotnet
rm: can't remove'/usr/bin/dotnet': Permission denied
$ docker exec 5bde77feebdf76ff370815f41a8989a880d51a4037c91e2ac8c6f2c269b759ad apk add curl
ERROR: Unable to lock database: Permission denied
ERROR: Failed to open apk database: Permission denied
后果:权限被回绝。这正是咱们想要的后果。让咱们再试一次,但晋升到 root。
$ docker exec -u root 5bde77feebdf76ff370815f41a8989a880d51a4037c91e2ac8c6f2c269b759ad ash -c "rm aspnetapp.pdb && ls aspnetapp.pdb"
ls: aspnetapp.pdb: No such file or directory
$ docker exec -u root 5bde77feebdf76ff370815f41a8989a880d51a4037c91e2ac8c6f2c269b759ad ash -c "touch /file && ls /file"
/file
$ docker exec -u root 5bde77feebdf76ff370815f41a8989a880d51a4037c91e2ac8c6f2c269b759ad ash -c "rm /usr/bin/dotnet && ls /usr/bin/dotnet"
ls: /usr/bin/dotnet: No such file or directory
$ docker exec -u root 5bde77feebdf76ff370815f41a8989a880d51a4037c91e2ac8c6f2c269b759ad apk add curl
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/community/x86_64/APKINDEX.tar.gz
(1/4) Installing brotli-libs (1.0.9-r9)
(2/4) Installing nghttp2-libs (1.51.0-r0)
(3/4) Installing libcurl (7.87.0-r2)
(4/4) Installing curl (7.87.0-r2)
Executing busybox-1.35.0-r29.trigger
OK: 14 MiB in 28 packages
您能够看到 root 能够做更多的事件,事实上,它能够做任何它想做的。装置 curl 后,攻击者能够从他们的任何网络服务器开始执行脚本。在 Alpine linux 的环境下,它与 wget 一起公布,它删除了这个链中的一个步骤。
请留神,我在这里应用的是 Alpine,所以应用 ash 而不是 bash 作为 shell,但这不会扭转演示的任何内容。
当然,解决办法是删除 root 用户以防止这些危险。然而,事实上,除非您采纳咱们的 Chiseled 镜像,否则删除 root 用户会产生未定义的行为。最好的抉择是以非 non-root 用户身份运行,它能够通过定义好的机制打消一整类的攻打。
应用 docker exec -u root 可能看起来很吓人。如果攻击者能够在您运行的容器上运行 docker exec -u root,那意味着他们曾经有了进入主机的权限。
sudo 并不包含在咱们的镜像中,而且永远也不会包含。
$ docker exec 5bde77feebdf76ff370815f41a8989a880d51a4037c91e2ac8c6f2c269b759ad sudo
OCI runtime exec failed: exec failed: unable to start container process: exec: "sudo": executable file not found in $PATH: unknown
在 Azure 容器服务中托管
在 Azure 容器服务中采纳这种模式非常简单。须要思考两个方面:端口和用户。
有些容器服务提供了比 Kubernetes 更高级的体验,须要不同的配置选项。
- Azure 应用服务要求 WEBSITES_PORT 应用 80 端口以外的端口。它能够通过 CLI 或在门户中设置。
- Azure 容器利用容许在创立资源的过程中更改端口。
- Azure 容器实例容许在创立资源的过程中更改端口。
这些服务都没有提供显著的形式来扭转用户。如果您在 Docker 文件中设置了用户(这是最佳的做法),那么就不须要该性能了。
后续步骤
下一步是钻研 non-root 可能具备挑战性的状况,例如诊断场景。一些例子应用 docker exec -u root。这在本地环境下很好用,然而 kubectl exec 不提供用户参数。咱们也将在之后的文章中更加深刻地钻研 non-root 的 Kubernetes 工作流程。
咱们还将持续与容器托管服务单干,以确保 .NET 开发人员可能轻松地转移到 .NET 8 容器镜像,特地是那些提供更高级别的体验,如 Azure App Service。
咱们 .NET 团队使命的一个要害局部是纵深进攻。每个人都须要思考平安问题,然而,咱们的业务是通过繁多的变动或性能来敞开整个攻打类别。大概十年前咱们刚开始公布容器映像时,就能够做出这种扭转。许多年来,咱们始终被要求提供 non-root 领导和 non-root 容器映像。诚实说,咱们并不分明该如何解决这个问题,很大水平上是因为咱们当初应用的模式在咱们刚开始的时候并不存在。在平安容器托管方面,没有一个领导者能够让咱们学习。正是与 Canonical 在 chiseled 镜像方面的单干教训,使咱们发现并造成了这种办法。
咱们心愿这一动作可能使整个 .NET 容器生态系统切换到 non-root 托管。咱们致力于使 .NET 应用程序在云中更具备高性能和安全性。