改进我的自托管 Actions Runner 设置

Lobsters Hottest 工具

摘要

作者通过用 systemd-nspawn 容器替换 Docker 来改进其自托管的 Gitea Actions Runner 设置,以提高安全性,并详细说明了配置和权衡。

<p><a href="https://lobste.rs/s/zcchf1/improving_my_self_hosted_actions_runner">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/23 18:48

# "改进我的自托管 Actions Runner 搭建" 来源:https://excipio.tech/blog/improving-my-self-hosted-actions-runner-setup/ 一段时间以来,我一直自托一个runner(https://gitea.com/gitea/runner)来与Gitea Action(https://docs.gitea.com/usage/actions/)集成。但有一件事一直让我担忧:在软件供应链安全日益重要的时代,我觉得需要提升这套部署的安全性。我最终找到的方案虽然还不完美,但相比之前已经有了很大改进。这篇文章将从头梳理我之前的做法、为什么及选择替换它、如何实现,以及还有哪些地方可以继续改善。 ## 最初,是 act# (https://excipio.tech/blog/improving-my-self-hosted-actions-runner-setup/#in-the-beginning-there-was-act) act runner(https://nektosact.com/)很久以前就诞生了,旨在提供在本地运行 GitHub Actions 的能力。这既允许快速反馈循环——开发者无需任何成本就能在本地模拟 GHA,同时也充当了本地任务运行器。对于 Gitea 这类项目来说,这似乎非常契合:在当今时代,一个没有集成 CI/CD 管线的 git 平台几乎无法用于“正经工作”。 大约三周前,这个分支变得更加显著。"act_runner" 简化为 "runner",semver 跃升至 v1.0.0,开发速度似乎显著加快——恐怕我们都知道原因,但希望这不会演变成软件的腐烂。无论如何,这与我的担忧息息相关。 我得承认我并不是一个称职的系统管理员。一段时间以来,我一直在系统上直接运行 runner——老派的裸金属方式。不过我不是傻瓜,runner 已经正确配置了专属用户、目录和权限。更重要的是,我只用它运行自己的管线,所以我有合理的信心认为这样做没问题。 但每天我都能读到关于 shai-hulud、GitHub token 被盗、蠕虫传播、CI/CD 管线被攻破的消息。虽然我只用 Go 写软件,而该生态目前相对安全,但这没有理由让我放松。相反,在问题发生之前就改善情况才是正确的时机。 ## 下一步?不是 Docker# (https://excipio.tech/blog/improving-my-self-hosted-actions-runner-setup/#whats-next-not-docker) 目前关于运行 runner 的建议是在 Docker 容器中运行任务。但我并不完全是 Docker 的粉丝;虽然它确实有其用途,让许多任务变得轻松,但它也引入了一些我想避免的权衡。 诚然,Docker 自诞生以来已经有了长足进步,自从它开始利用 **cgroups** 并支持 **rootless** 模式后,大部分安全顾虑已经消失。但它仍然没有提供合理的默认配置,且 rootless 模式仍有其问题。因此,如果我要花一些时间来正确配置 Docker(我已经做过了),我宁可用这些时间学习新东西。所以,让我们利用现代 Linux 所提供的设施,而不必求助于 Docker。 如果说“我不喜欢 Docker”还没得罪够人,那么我接下来要惹更多人不快了。是的,我求助于 `systemd`。更具体地说,我使用了三个 systemd 组件,它们让这件事变得(相对)容易、(非常)安全,以及(不那么)有趣:`systemd-nspawn` 用于利用 **内核命名空间**、**cgroups** 和 **seccomp**;`systemd-networkd` 用于轻松创建私有虚拟链路;以及 `systemd-resolved` 用于解决我系统中的一个问题。 ## 容器解决方案# (https://excipio.tech/blog/improving-my-self-hosted-actions-runner-setup/#the-container-solution) systemd-nspawn 的手册页(https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html)对其定义和功能有非常贴切的描述,我在此直接引用: > systemd-nspawn 可用于在轻量级命名空间容器中运行命令或操作系统。它在许多方面类似于 chroot(1),但更强大,因为它虚拟化了文件系统层次结构、进程树、各种 IPC 子系统以及主机和域名。 现在明确一点:这里没有任何 systemd 独有的东西。从某种意义上说,nspawn 就像 Docker,因为它重新打包并围绕非常强大的技术创建了抽象层。好处在于它与 systemd 生态系统的其余部分拥有一流的集成,我们将充分利用这一点。 不过总的来说,我想要一个容器,能够运行一个具有以下特征的进程: - 无能力(no capabilities) - 无法访问主机文件(单独且只读的根文件系统) - 无法访问主机进程(独立的 PID 命名空间) - 无法访问主机网络服务(独立的网络命名空间) - 无新特权(no new privileges) - 受限的系统调用集合 这可以在不到 10 行的配置文件中实现: `` [Exec] # 禁用用户命名空间——避免与绑定挂载的 UID 映射问题。 PrivateUsers=no # 阻止 setuid/setgid 二进制文件(sudo, ping 等) NoNewPrivileges=yes # Seccomp 过滤器:只允许列出的系统调用组 SystemCallFilter=@system-service @process @basic-io @file-system @network-io @signal @ipc @mount [Network] # 具有 veth 对和自动 NAT 的私有网络命名空间 VirtualEthernet=yes [Files] # 根文件系统不可变——所有写入必须通过绑定挂载 ReadOnly=yes # 可写绑定挂载(在重建根文件系统后仍然存在) Bind=/var/lib/gitea-runner-container:/var/lib/gitea-runner Bind=/var/cache/gitea-runner-container:/var/cache/runner `` ## 网络难题# (https://excipio.tech/blog/improving-my-self-hosted-actions-runner-setup/#the-networking-headaches) 现在,网络部分总是最有趣的,不是吗?既然我不想让生活变得太轻松,有一件事我不愿放弃:让容器使用我的私有 DNS 解析器。如果你不全力以赴,自托管还有什么意义?而且别忘了,我们仍然需要一个独立的网络命名空间——如果容器被攻破,仅仅共享主机的网络会很危险。 幸好,`systemd-networkd` 和 `systemd-resolved` 前来救场。通过在宿主机和容器上都运行 `networkd`,配置变得非常简单,特别是考虑到我想要一个相对静态的配置来设置 DNS 解析。在我们的案例中,`resolved` 是一个极其简单的存根解析器,在宿主机上指向一个只能通过 Wireguard 隧道访问的权威 DNS 解析器。 第一步是屏蔽通用的容器网络配置,只需执行 `ln -sf /dev/null /etc/systemd/network/80-container-ve.network`。之后,通过编辑 `/etc/systemd/network/90-ve-gitea-runner.network` 来配置我们容器的接口: `` [Match] Name=ve-gitea-runner [Network] Address=172.30.0.1/28 DHCPServer=yes IPMasquerade=both [DHCPServer] EmitDNS=yes DNS=172.30.0.1 `` 这里重要的不是容器最终获得的地址,而是网关的 IP:我们将把 DNS 解析指向那里。因此,在 **宿主机** 的 `resolved` 配置中,我们需要添加一个存根,以确保 resolved 会响应来自容器的查询: `` [Resolve] DNSStubListenerExtra=udp:172.30.0.1:53 `` 而在 **容器** 的 `resolved` 中,我们需要将其指向宿主机: `` [Resolve] DNS=172.30.0.1 Domains=~. DNSStubListenerExtra=udp:[::1]:53 `` 添加最后一行是因为容器中的某些进程期望 IPv6 解析;这样可以让 `resolved` 也将这些查询转发给宿主机。 就是这样!我想你现在明白我为什么还额外利用了 systemd 的那些好处了。在宿主机和容器上都启用适当的服务后,一切按预期运行。除了一件事:如果你有防火墙,还需要配置它。我使用的 iptables,但同样的规则配置 nftables 应该也不难: `` -A INPUT -i ve-+ -p udp --dport 67 -j ACCEPT -A INPUT -i ve-+ -p udp --dport 53 -j ACCEPT -A FORWARD -i ve-+ -o eth0 -j ACCEPT -A FORWARD -i eth0 -o ve-+ -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT `` ## 总结与一些考虑# (https://excipio.tech/blog/improving-my-self-hosted-actions-runner-setup/#wrapping-it-up-and-some-considerations) 现在,配置好所有这些后,剩下的就是从容器内向 Gitea 平台注册 runner,然后开始构建!事实上,如果你正在阅读这篇文章,说明这次迁移成功了,我现在正在使用容器化的 CI/CD 管线!万岁! 当然,我把一切说得如此美好平静,但这仍然存在一些缺点,即: - 你的缓存和工作空间仍然可能被恶意依赖污染 - 更新容器意味着必须停止容器,禁用 ReadOnly 模式,执行更新命令,启用 ReadOnly,然后再次启动容器 - 这不是一个真正的虚拟机,因此仍然存在一些方法可以串联利用漏洞,理论上能够实现容器逃逸、权限提升等 虽然我认为这些都不是致命问题,但它们确实留下了改进空间。鉴于 runner 已经支持临时 runner(https://docs.gitea.com/usage/actions/act-runner#ephemeral-runners),将其与 `systemd-nspawn` 集成肯定是一个可以做的事情,并能极大地帮助提升系统的安全性。也许有一天 :-)

相似文章

用于 Gleam 单仓库的 GitHub Actions

Lobsters Hottest

一位开发者分享了他们在 Gleam 单仓库中测试 BEAM 与 JavaScript 两套运行时的 GitHub Actions 配置,采用矩阵策略并严格执行格式检查。

@DeRonin_: 任何使用或学习智能体系统的人都应该读一读这个。我在每个新智能体项目前执行的安装顺序:1.…

X AI KOLs Following

一条分享智能体项目结构化安装顺序的推文:使用 direnv 配合密码管理器保障凭证安全,使用 litellm 或 portkey 作为模型代理管理成本和回退,使用 uv + git 在评估通过时提交以确保可复现性,使用 mitmproxy 实现 LLM 调用的全面可观测性。重点介绍了常见故障模式和安全漏洞。

在树莓派上托管网站

Hacker News Top

技术教程,介绍如何在树莓派上自托管网站,涵盖端口转发、DNS 配置、使用 Caddy 作为反向代理、PM2 进行 Node.js 进程管理,以及使用 GitHub Actions 实现 CI/CD 自动化。