容器镜像安全加固
摘要
本文介绍了加固容器镜像的技术,例如静态编译应用程序、使用最小化基础镜像,以及实施Landlock以限制文件系统和网络访问,从而提高安全性。
<p><a href="https://lobste.rs/s/8hz82r/hardening_container_images">评论</a></p>
查看缓存全文
缓存时间: 2026/09/08 00:57
# 加固容器镜像
来源:https://www.grepular.com/Hardening_Container_Images
发布于 2天前 2026年9月5日 阅读时长 8分钟 [查看 Markdown 原文](https://www.grepular.com/Hardening_Container_Images.md) [其他文章](https://www.grepular.com/blog/#Hardening_Container_Images)
本文由人类撰写:[Mike Cardwell (https://www.grepular.com/)](https://www.grepular.com/)
我最近使用 [Flatcar Container Linux (https://www.flatcar.org/)](https://www.flatcar.org/) 设置了一台新服务器。我决定在此系统上部署的任何容器都应该从一开始就进行“加固”。我决定添加的第一个容器是 [PowerDNS Recursor (https://www.powerdns.com/powerdns-recursor)](https://www.powerdns.com/powerdns-recursor)。其 [官方镜像 (https://hub.docker.com/r/powerdns/pdns-recursor-54)](https://hub.docker.com/r/powerdns/pdns-recursor-54) 包含许多我不需要的、不必要的暴露面。我并非在此贬低 PowerDNS,只是预计大多数我想要添加的容器都会面临类似的情况。
官方镜像最值得注意的一点是,它本质上就是 [Debian (https://www.debian.org/)](https://www.debian.org/),上面附加了 pdns-recursor。因此,这是一个约 355 MB、包含约 7,500 个文件的镜像。你可以使用 [Musl (https://www.wikipedia.org/wiki/Musl)](https://www.wikipedia.org/wiki/Musl) 将 `pdns-recursor` 静态编译成单个可执行文件。如果你想同时包含 `rec_control`,那就会有两个可执行文件。这就是我开始构建 [pdns-recursor-trimmed (https://gitlab.com/grepular/pdns-recursor-trimmed)](https://gitlab.com/grepular/pdns-recursor-trimmed) ([容器镜像 (https://hub.docker.com/r/grepular/pdns-recursor-trimmed)](https://hub.docker.com/r/grepular/pdns-recursor-trimmed)) 的原因。它在 `latest` [Alpine (https://alpinelinux.org/)](https://alpinelinux.org/) 容器内编译,然后将可执行文件复制到 scratch (https://hub.docker.com/_/scratch) 镜像中。我们使用 [PGP (https://www.wikipedia.org/wiki/Pretty_Good_Privacy)](https://www.wikipedia.org/wiki/Pretty_Good_Privacy) 根据 [文档化的发布密钥 (https://doc.powerdns.com/recursor/appendices/compiling.html)](https://doc.powerdns.com/recursor/appendices/compiling.html) 验证源代码 tarball。
这最初使我得到了一个 36MB 的镜像,仅包含 2 个文件(后来又添加了几个支持文件)。如果该进程被入侵,攻击者没有 shell 可用,也没有系统工具可以利用,这将使他们的攻击变得更加困难。
实际上,镜像本可以比这更小,但我决定排除 [SNMP (https://www.wikipedia.org/wiki/Simple_Network_Management_Protocol)](https://www.wikipedia.org/wiki/Simple_Network_Management_Protocol) 支持和 zoneToCache (https://docs.powerdns.com/recursor/lua-config/ztc.html) 配置项中的 `url` 选项。原因是,这两者都引入了历史上具有大量 [CVE (https://www.cve.org/)](https://www.cve.org/) 的依赖项,而且我认为这些功能使用频率不高。我可能错了,但这是我的决定。
我想要避免使用有许多 CVE 的软件的原因是,它们会在漏洞扫描器中产生噪音。我稍后会解释为什么这很重要。
接下来,我添加了一个 [launcher (https://gitlab.com/grepular/pdns-recursor-trimmed/-/blob/main/launcher.c)](https://gitlab.com/grepular/pdns-recursor-trimmed/-/blob/main/launcher.c)。这是一个用 C 编写的小型自定义程序,作为容器的入口点。它实现的功能之一是在移交给 pdns 之前,利用 Linux 内核的 [Landlock (https://docs.kernel.org/userspace-api/landlock.html)](https://docs.kernel.org/userspace-api/landlock.html) 特性对文件系统和网络访问进行沙箱化。launcher 使用 Landlock 将读取访问限制在仅需要读取的路径(基本上是配置文件,不需要 /proc/ 等),将写入访问限制在仅需要写入的路径。它还使用 Landlock 限制我们可以连接和绑定的 TCP 端口,并且(如果你的内核足够新)限制可以绑定的 UDP 端口。这可以防止攻击者向某个路径写入恶意软件然后执行它。也阻止了他们连接到通常用于命令和控制系统的端口。
我想做的下一件事是将系统调用限制为应用程序所需的那些。如果应用程序本身从来不需要访问 `io_uring`、`bpf`、`userfaultfd`、`keyctl` 等,为什么要让攻击者能够访问这些他们可能利用来提权的接口呢?为此,我创建了一个 [seccomp.json](https://gitlab.com/grepular/pdns-recursor-trimmed/-/blob/main/seccomp.json) 文件,其中包含一个应用程序使用的系统调用的允许列表。此文件不包含在镜像中,你需要获取它,然后在镜像的 compose 文件或 k8s yaml 中引用它。生成此文件使用了 `strace`,并且我还创建了广泛的烟雾测试,在不同架构和内核上测试 pdns-recursor 的各种功能,以确保允许列表是正确的。`memfd_create` 不在允许列表中,这是攻击者在没有 shell 和 noexec 的镜像中,通常用于绕过限制实现无文件攻击的路径。
文档中还推荐了一些经过测试的其他运行时容器加固选项,以 compose 格式表示:
1. `read_only: true` - 应用程序不需要在任何地方写入,那为什么要允许它写入呢?Landlock 在你的内核中可能不被支持,因此可能没有限制文件系统访问。
2. `tmpfs: /tmp:rw,noexec,nosuid,nodev,size=8m` - 我撒了谎。它需要向 `/tmp` 写入 `rec_control` 的 socket。在微型 tmpfs 上使用 `noexec`,这样如果攻击者在其中写入了内容,至少无法执行。
3. `seccomp: /path/to/seccomp.json` - 限制系统调用。上面已讨论。
4. `cap_drop: [ALL]` - 为什么在一个不需要它们的镜像中暴露 [Linux Kernel Capabilities (https://man7.org/linux/man-pages/man7/capabilities.7.html)](https://man7.org/linux/man-pages/man7/capabilities.7.html)。
5. `ipc: none` - 如果你的应用程序不需要进程间通信(IPC),为什么要暴露这个潜在的攻击面。
6. `sysctls: net.ipv4.ip_unprivileged_port_start: 53` - 在仍将 1024 以下端口视为特权端口的运行时上,这使我们能够以非 root 用户身份绑定到 53 端口。
7. `security_opt: no-new-privileges:true` - 防止用户通过 setuid 二进制文件(通常设计用于允许提权)等方式提升权限。
与官方镜像相比,另一个重要的区别是:当发布 pdns-recursor 版本 A.B.C 时,会构建并推送该版本的容器镜像,到此为止。它捕获了构建镜像时 Debian 附带的依赖版本。我在 GitLab 上设置了 CI 流水线,以跟踪 PowerDNS 最新的两个稳定“分支”。撰写本文时,这些是 `5.4.x` 和 `5.3.x`。每天,这两个分支的最新版本都会自动重新构建,如果有任何变化,镜像就会被推送。时间戳设置为 epoch,以避免触发“更改”通知。
例如,如果在 PowerDNS 的依赖项(如 OpenSSL)中发现了一个关键 CVE,Debian 和 Alpine 等发行版会发布新的 OpenSSL 包。我的下一次构建时,它会自动从 Alpine 软件包索引获取最新的 OpenSSL 并进行编译。因此,如果你使用我的镜像并标记 `5.4.6`,那么你可能会在不同日期获得不同的 `5.4.6` 构建版本,并带有安全改进。如果你想固定到特定构建版本,当然可以使用 `5.4.6_` 来避免更改。如果你想保持在最新的 `5.4.x` 分支,也可以使用 `5.4` 这样的标签(这是我所做的),甚至使用 `5` 或 `latest` 来获取最新镜像。尽管这样做有风险,因为它可能包含不兼容的向后更改。
CI 还会针对每天构建的 4 个镜像(amd64/arm64 以及 当前/上一个 分支)运行我的烟雾测试。烟雾测试旨在尽可能多地测试新构建镜像的功能。我们还为每个架构启动了 5 个单独的 VM,以针对不同的内核运行更少的测试,确保 Landlock、seccomp 和其他一些功能仍然正常工作。这一点很重要,因为 GitLab runner 的内核可能不支持 Landlock。
目前存在各种用于扫描容器镜像已知漏洞的应用程序 - [Trivy (https://trivy.dev/)](https://trivy.dev/)、[Grype (https://anchore.com/grype/)](https://anchore.com/grype/)、[OSV Scanner (https://google.github.io/osv-scanner/)](https://google.github.io/osv-scanner/)。它们通常会查看镜像中的各种发行版软件包索引,以及如果存在的话,称为 SBOM(软件物料清单)的东西。我们的镜像没有基础发行版(因为它基于 scratch 镜像),因此这些应用程序不会发现任何漏洞。
因此,我实现了在构建时自动生成 SBOM。它从链接器生成并作为 [CycloneDX (https://cyclonedx.org/)](https://cyclonedx.org/) 文件写入镜像中,像 Trivy 和 Grype 这样的扫描器可以在那里找到它。它还会被转换为 [SPDX (https://spdx.dev/)](https://spdx.dev/) 格式,并作为 [attestation (https://docs.docker.com/dhi/explore/security-concepts/attestations/)](https://docs.docker.com/dhi/explore/security-concepts/attestations/) 附加到注册表中的镜像,其中 [SLSA (https://docs.docker.com/dhi/explore/security-concepts/slsa/)](https://docs.docker.com/dhi/explore/security-concepts/slsa/) 证明记录了 git 修订版、Dockerfile 和构建参数。
官方 PowerDNS Recursor 镜像有趣的地方在于(正如我之前所说),它只是 Debian 加上复制进去的一些额外文件。因此,PowerDNS 甚至没有出现在该镜像的 Debian 包列表中。如果 PowerDNS 存在 CVE,扫描器在扫描该镜像时不会检测到。此外,PowerDNS recursor 包含一些可能有 CVE 的 Rust crate,甚至还有可能也有 CVE 的内置 JavaScript 文件。我确保在构建时自动将所有这些添加到 SBOM 中。
回到我之前关于 CVE 噪音的评论。在我创建这个 SBOM 之后,扫描器开始发现镜像中包含的 [Handlebars.js (https://handlebarsjs.com/)](https://handlebarsjs.com/) 存在关键和高危 CVE。我查看了这些 CVE,它们似乎并不影响 Handlebars 在这里的使用方式。事实证明,对此也有解决方案。我编写了一个 [OpenVEX (https://openssf.org/projects/openvex/)](https://openssf.org/projects/openvex/) 文件来声明哪些发现不适用以及原因。它存放在 git 仓库中 ([https://gitlab.com/grepular/pdns-recursor-trimmed/-/blob/main/openvex.json](https://gitlab.com/grepular/pdns-recursor-trimmed/-/blob/main/openvex.json)),在 CI 中传递给扫描器,并再次作为 attestation 附加到镜像。可以配置扫描器使用此文件来忽略这些发现。
关于签名。在 CI 构建期间,GitLab 提供一个短期的 OIDC 身份令牌。我们用它从 [Fulcio (https://github.com/sigstore/fulcio)](https://github.com/sigstore/fulcio) 换取一个证书。该证书被 [cosign (https://github.com/sigstore/cosign)](https://github.com/sigstore/cosign) 用于签名多架构索引摘要。这也涵盖了 SBOM、证明和 VEX attestation。然后它作为 Sigstore bundle 上传到 Docker 注册表,并记录到 [Rekor (https://docs.sigstore.dev/logging/overview)](https://docs.sigstore.dev/logging/overview)(Sigstore 的公共透明日志)中。这意味着你可以在运行镜像之前运行以下命令进行验证:
```
cosign verify \
--certificate-identity \
'https://gitlab.com/grepular/pdns-recursor-trimmed//.gitlab-ci.yml@refs/heads/main' \
--certificate-oidc-issuer \
https://gitlab.com \
grepular/pdns-recursor-trimmed:5.4
```
这保证了容器镜像是通过我在 main 分支上的 GitLab CI 流水线构建的。如果我的 Docker Hub 帐户被入侵,有人上传了可疑镜像并移动了标签,那么你将能够检测到。当然,如果我的 GitLab 帐户被入侵,那就是另一回事了。顺便说一句,我使用了 2FA。
每次构建后,会对两个分支的最新两个镜像使用 Trivy、Grype 和 OSV Scanner 进行漏洞扫描。如果发现任何未在 VEX 中解决的新增问题,流水线会失败。因此,我会收到任何新的相关 CVE 的警报,并手动处理它们。
这一点与安全无关,但值得一提。由于镜像内容极少,没有 `dig` 或 `curl` 等工具可用于实现健康检查。因此,我向 launcher 添加了一个参数,使其既可以作为应用程序启动器,也可以作为健康检查启动器:`/launcher --healthcheck`。它对服务执行真正的 DNS 查询,任何格式良好的响应都会成功。
哦,最后。如果有什么原因导致日常的 GitLab runner 停止运行怎么办?例如,我可能用完了 runner 分钟数而没有注意到。我注册了 https://healthchecks.io,流水线的最后一步是 ping 该服务。如果该服务在大约 24 小时内没有收到 ping,它会提醒我出现问题。
那么,我取得了什么成果呢?首先,我拥有了一个更难被入侵的服务,因为它拥有更少且更新的依赖项。其次,如果攻击者入侵了它,他们提权或造成进一步损害的可能性更低。第三,已知漏洞更容易被检测到并能更快处理。作为项目外部人员,我做这件事比内部人员更困难。他们可能会发布一个带有新依赖的新版 pdns-recursor,而我可能无法检测到,因此它不会自动出现在我的 SBOM 中。或者他们可能添加了一个依赖于我的 seccomp 文件不允许且我没有烟雾测试的系统调用的新功能。作为外部人员,我唯一的解决方案就是关注新版本发布并分析变更内容。
喜欢这篇文章
相似文章
使用Rootless容器保护服务安全
一篇博客文章讨论了通过运行无root权限的容器来保护服务安全的最佳实践,从而降低安全风险。
使用 LXC 增强 X11 应用程序安全性
一份技术指南,介绍如何使用非特权 LXC 容器隔离 X11 应用程序(如网页浏览器),通过将容器 UID/GID 映射到未使用的主机范围来增强安全性。
完全在浏览器中的容器构建
一个完全在浏览器中使用客户端代码构建容器的Web应用程序,展示了自定义容器工具的强大功能。用户可以选择基础镜像、运行Shell脚本,并将生成的镜像导出为tar文件。
500 行代码搞定 Linux 容器
一篇技术博客,通过约 500 行 C 代码讲解 Linux 容器的工作原理,涵盖用户命名空间、网络命名空间以及 capabilities 绕过风险,并附带可运行的漏洞利用演示。
@tom_doerr: 加固Linux服务器的全面指南,涵盖SSH、防火墙和入侵检测工具。https://github…
一份全面的Linux服务器安全指南,涵盖SSH加固、使用UFW进行防火墙配置,以及使用Fail2Ban和CrowdSec等工具进行入侵检测。