Ubuntu 24.04 部署自托管 GitHub Actions Runner
在 Ubuntu 24.04 上注册自托管 GitHub Actions runner,涵盖专用用户、校验和、config.sh、systemd 服务及 fork pull request 执行工作流的安全风险。
自托管 GitHub Actions runner 的作用
自托管 GitHub Actions runner 是您安装在自己 VPS 上的程序。它向 GitHub 请求作业,并在您的硬件上运行这些作业。您将其注册到一个仓库,将其安装为 systemd 服务。每次重启后,它都会自动恢复运行。GitHub 负责调度作业。您的服务器负责执行作业。
在自己拥有的服务器上运行 CI(持续集成)有两个主要原因。构建分钟数不再按量计费。作业还可以访问只有您的机器具备的资源,例如预热的构建缓存或私有网络。代价是安全风险。runner 会以您为其指定的用户身份,执行工作流文件中的所有内容。因此,工作流文件从设计上就是远程代码执行。在私有仓库中,这通常没有问题,因为只有您信任的人才能添加工作流文件。在公共仓库中,这存在实际风险;关于 fork pull request 的章节会解释其工作机制。
以下内容基于 Ubuntu 24.04 和 runner 版本 2.336.0;截至 July 2026,这是当前发布的版本。
开始前的准备
从一台配有普通管理员账户和 sudo 的 VPS 开始,状态应达到新 VPS 上线后的前十分钟所述的阶段。无需开放入站端口。runner 会向 GitHub 建立出站 HTTPS(超文本传输协议安全版)连接,并在等待任务时保持连接。GitHub 不会连接到您的服务器。因此,防火墙可以继续对外关闭,任务仍然能够到达。
您还需要拥有仓库的管理员权限,因为注册令牌显示在仓库设置中。
为 runner 创建专用用户
不要以 root 或您自己的管理员用户运行 runner。每个作业都会继承 runner 用户的权限,因此,如果 runner 用户可以使用 sudo,调用 sudo 的工作流就会成功。创建一个非特权用户,使其除了自己的主目录外不拥有任何内容。VPS 上的最小权限用户账户介绍了通用做法。下面是针对 runner 的具体配置。
sudo useradd -m -s /bin/bash gharunner
sudo passwd -l gharunner
sudo chmod 750 /home/gharunner
sudo install -d -m 700 -o gharunner -g gharunner /home/gharunner/actions-runnerpasswd -l 会锁定密码,因此无法使用该密码以 gharunner 身份登录。runner 目录的 700 权限很重要,因为 runner 会在其中以明文存储凭据,而 checkout 目录中可能包含私有源代码。
继续之前,请检查这两个属性:
sudo passwd -S gharunner
sudo -l -U gharunnerpasswd -S 会输出一行以 gharunner L 开头的内容,其中 L 表示密码已锁定。sudo -l -U gharunner 应返回 is not allowed to run sudo。如果输出的是允许执行的命令列表,则该账户属于 sudo 组,刚刚建立的隔离就已失效。
下载 runner 并检查 tarball
从这里开始,以 runner 用户身份操作。
sudo -iu gharunner
cd ~/actions-runner
RUNNER_VERSION=2.336.0
curl -fL -o actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \
"https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"如果不确定架构,请先运行 uname -m。x86_64 使用上面的 linux-x64 文件。aarch64 使用 actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz。
现在验证已下载的文件。下面的 SHA256(安全哈希算法,256 位)值对应 2.336.0 x64 tarball。GitHub 会在发布页面和“New self-hosted runner”界面显示当前版本的值。该值会随每个版本变化,因此安装其他版本时,请从这些位置复制对应的值。
echo "04cf0be1aff4c3ec3554466c39124ca250e3effd8873bb7e8d68535aa9505d5d actions-runner-linux-x64-2.336.0.tar.gz" | sha256sum -c下载正确时,会输出一行:
actions-runner-linux-x64-2.336.0.tar.gz: OK文件被截断或修改时,会输出失败信息和警告:
actions-runner-linux-x64-2.336.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match不要跳过检查,等 tar 来发现问题。写入不完整的归档文件会导致 gzip: stdin: unexpected end of file 和 tar: Unexpected EOF in archive 失败。这只能说明文件已损坏,无法判断文件是被截断还是被替换。
tar xzf ./actions-runner-linux-x64-2.336.0.tar.gz
lstarball 包含的内容,以及不包含的内容
解压后,目录中包含 config.sh、run.sh、env.sh、safe_sleep.sh、bin/ 和 externals/。bin/ 包含运行程序二进制文件和 bin/installdependencies.sh。externals/ 包含 JavaScript action 执行所使用的内置 Node 运行时。
此时还没有 svc.sh。GitHub 文档将其描述为“成功添加 runner 后创建的”脚本,因为该脚本根据模板生成,并将您的仓库名称和 runner 名称写入服务名称。因此,在 ./config.sh 之前执行 sudo ./svc.sh install 会因 sudo: ./svc.sh: command not found 失败。请先注册 runner,再安装服务。
安装 runner 依赖项
runner 是一个 .NET 应用,因此需要一些共享库。保留 runner 用户的 shell,并使用 sudo 安装这些依赖项,因为该脚本会写入系统软件包数据库。
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.sh在 Ubuntu 24.04 上,这会安装 libkrb5-3、zlib1g、liblttng-ust1t64、libssl3t64 和 libicu74。该脚本会为每个库尝试多个版本名称,并保留当前发行版提供的版本,因此同一个脚本也适用于较旧的 Ubuntu 和 Debian。
跳过此步骤后,./config.sh 会在执行任何操作前停止:
Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.缺少 libicu 时,脚本会给出相同的建议,但首行不同,即 Libicu's dependencies is missing for Dotnet Core 6.0。这两个提示的根本原因相同:启动前,config.sh 会针对捆绑库运行 ldd。因此,无法解析的链接会使脚本停止,而不是稍后产生难以判断原因的崩溃。
将 runner 注册到仓库
从仓库获取令牌。依次打开 Settings、Actions、Runners,然后选择 New self-hosted runner。页面会显示一个以 A 开头的注册令牌。令牌会在创建 1 小时后过期,因此请在准备粘贴时再生成。
使用 runner 用户进行注册。config.sh 不允许通过 sudo 运行。
sudo -iu gharunner
cd ~/actions-runner
./config.sh --url https://github.com/YOUR-USER/YOUR-REPO \
--token PASTE_REGISTRATION_TOKEN_HERE \
--name vps-runner-1 \
--labels vps \
--work _work \
--unattended \
--replace这些标志的作用如下。--name 决定 runner 在仓库中的显示名称,因此请选择一个在 6 个月后仍能识别的名称。--labels 添加自定义标签;runner 会自动包含 self-hosted、Linux 和 X64,无需手动指定。--work 指定检出内容所在的目录,该目录位于 runner 目录内。--unattended 使用默认值回答交互式提示;当命令位于脚本中时,应使用此选项。--replace 接管同名的现有注册,而不是直接失败;重建服务器时应使用此选项。
命令成功运行后,末尾会显示以下内容:
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.注册信息现在保存在 runner 目录中,文件为 .runner、.credentials 和 .credentials_rsaparams。后两个文件用于向 GitHub 标识此 runner,因此任何能够读取它们的人都可以冒充该 runner。这就是目录权限设为 700 且该用户不具备 sudo 权限的原因。
将 runner 安装为 systemd 服务
在终端中运行 ./run.sh 适合进行一次测试,但它会随 SSH 会话结束而退出。安装该服务,使 runner 在系统启动时自动运行。VPS 上的 systemd 服务和计时器介绍了 unit 文件本身。这里的 svc.sh 会为您生成 unit 文件。
exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh statussvc.sh 需要 root 权限,因为它会将 unit 写入 /etc/systemd/system 并启用该 unit。install 后面的参数指定服务运行所使用的用户。请显式传入 gharunner。如果不传参数,脚本会回退到 $SUDO_USER,也就是您的管理员账户。这样每个作业都会以可以使用 sudo 的用户身份运行。
unit 的名称由仓库和 runner 组成,格式为 actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service。您不需要手动输入该名称:
systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pager运行正常的 runner 会记录 √ Connected to GitHub,然后记录一行以 Listening for Jobs 结尾的日志;仓库的 Runners 页面会将其显示为 Idle。显示为 Offline 的 runner 要么未运行,要么无法通过 443 端口连接 GitHub。
向 runner 发送作业
runs-on按标签选择 runner。请同时指定 self-hosted 和您自己的标签,避免作业被分配到并非目标的 runner。
name: build
on:
push:
branches: [main]
jobs:
build:
runs-on: [self-hosted, linux, vps]
steps:
- uses: actions/checkout@v5
- run: uname -a如果作业停留在 Waiting for a runner to pick up this job,说明标签不匹配。runs-on 中的每个标签都必须存在于 runner 上,因此多出一个单词也会导致作业持续排队,而且不会在任何位置报错。请将该列表与仓库设置中 runner 旁显示的标签进行对比。
为什么自托管 runner 不应与公共仓库混用
这是人们经常跳过的部分。GitHub 的指导非常明确:自托管 runner “几乎不应当用于公共仓库”,并且它们“无法保证在临时的干净虚拟机中运行,可能会被工作流中的不可信代码持续入侵”。
其机制很简单。来自 fork 的 pull request 会携带一份自己的工作流文件副本。如果您的公共仓库在自托管 runner 上运行 pull request 工作流,那么任何能够 fork 该仓库的人都可以提交一个工作流,让自己的命令在您的 VPS 上运行。他们不需要写入权限,因为他们提交的内容本身就是实际运行的内容。
审批设置只能缓解问题,不能解决问题。公共仓库的默认策略会要求维护者批准首次贡献者的 fork 工作流。您批准某人一次后,该用户后续的 pull request 就不会再次触发提示。因此,这道门槛依赖人工每次查看差异,而隐藏在构建脚本三层调用之下的恶意载荷很容易被忽略。
来自 fork 的 pull request 不会获得您的机密,其 GITHUB_TOKEN 也是只读的。这可以限制 GitHub 内部的损害,但无法保护您的服务器。攻击者可以获得 gharunner 的 shell,因此能够读取该用户有权读取的所有文件,访问 VPS 在私有网络上能够访问的任何资源,并在 ~/.bashrc 中写入后门,或创建一个会在下次作业期间运行的用户级 systemd 单元。
使用 --ephemeral 注册后,runner 会接受一个作业,然后注销,这样单个作业就无法读取下一个作业的工作区。只有在每个作业都会重新构建机器或容器时,这种方式才有帮助,因为写入 runner 用户主目录的后门会在重新注册后继续存在。
后续规则很简单。对私有仓库使用自托管 runner。如果必须将 runner 连接到公共仓库,请不要让它运行 fork 的 pull request,也不要在该服务器上运行其他内容,并将这台机器视为一次性资源。
Docker 作业,以及实际上等同于 root 的组
容器作业、服务容器以及调用 docker build 的任何工作流步骤,都需要运行器主机上的 Docker daemon。按常规方式安装 Docker,具体步骤参见 在 VPS 上安装 Docker 和 Docker Compose,然后将运行器用户加入 docker 组。
执行此操作前,请了解其中的权衡。加入 docker 组等同于获得 root 权限,因为容器可以绑定挂载 /,并在容器内以 root 身份运行。因此,能够访问 Docker socket 的工作流可以读写 VPS 上的所有文件,包括 /etc/shadow。对于贡献者可信的私有仓库,这种风险可能可以接受。在其他场景中,这会使非特权用户失去实际意义。Rootless Docker 会将容器构建限制在运行器用户自身的权限范围内,但代价是存储驱动速度较慢,并且不支持特权容器。
更新以及正确移除 runner
自托管 runner 默认会自动更新。它检测到新版本后,会替换自身文件并重启服务,因此通常无需手动操作。需要固定版本时,./config.sh --disableupdate 可关闭自动更新。此后必须手动更新:GitHub 文档明确说明,配置了 --disableupdate 的 runner 必须手动更新。
手动更新不会丢失注册信息,因为 .runner 和 .credentials 不在 tarball 中。停止服务,下载新 tarball 并按 gharunner 校验,然后使用 tar xzf 将其解压到原目录,最后重新启动服务:
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh start要移除 runner,先卸载服务,再取消注册。移除令牌位于同一个 Runners 页面中,点击该 runner 自己的 Remove 按钮即可获取。
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh uninstall
sudo -iu gharunner
cd ~/actions-runner
./config.sh remove --token PASTE_REMOVAL_TOKEN_HERE如果未取消注册就删除目录,该 runner 仍会在仓库中显示为 Offline,因为 GitHub 只有在 runner 报告自身已移除,或管理员手动删除该条目时,才会知道它已不存在。
故障模式及其对应提示信息
Must not run with sudo。以 root 身份运行时,config.sh 会输出此信息并退出。此检查是有意设计的,因为 _work 中由 root 拥有的文件会导致之后以服务用户身份运行的所有任务失败。请以 gharunner 身份运行 ./config.sh。RUNNER_ALLOW_RUNASROOT 变量可以绕过此检查,但只会让故障延后发生。
sudo: ./svc.sh: command not found。当前目录正确。svc.sh 尚不存在,因为 config.sh 尚未完成注册。请先注册 runner,再安装服务。
Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'。此令牌不是有效的注册令牌。它可能已过期,因为注册令牌的有效期只有 one hour;也可能是将个人访问令牌粘贴到了 Runners 页面中的注册令牌位置。请生成新令牌并重新粘贴。
Dependencies is missing for Dotnet Core 6.0。以 root 身份在 runner 目录中运行 sudo ./bin/installdependencies.sh,然后重新注册。
重启后 Runner Offline。请运行 systemctl is-enabled 'actions.runner.*'。如果没有列出任何内容,说明从未运行过 ./svc.sh install,因此 runner 只存在于当前终端会话中。如果该单元已启用,但 runner 仍处于 Offline 状态,请查看 journalctl -u 'actions.runner.*' 并检查出站 HTTPS 连接。
磁盘空间耗尽。检出内容、构建缓存和 Docker 镜像会累积在 _work 及 runner 用户的 home 目录中,系统不会自动为您清理。请监控 du -sh /home/gharunner/actions-runner/_work,并在磁盘空间耗尽前添加定时清理任务。
FAQ
为什么 sudo ./svc.sh install 提示 command not found?
因为 svc.sh 不在 runner tarball 中。./config.sh 完成注册后,会在 runner 目录中生成它,并使用您的仓库名称和 runner 名称构造服务名称。请先以 runner 用户运行 ./config.sh。之后,sudo ./svc.sh install gharunner 会找到该脚本,并在 /etc/systemd/system 中写入名为 actions.runner.OWNER-REPO.RUNNER-NAME.service 的 unit。
自托管 runner 需要开放防火墙端口吗?
不需要。runner 会向 GitHub 建立出站 HTTPS 连接,并在等待作业时保持连接,因此 GitHub 不会主动连接您的 VPS。允许出站 443 端口,并保持入站规则关闭。如果 runner 的服务正在运行,但状态显示为 Offline,请检查出站过滤和 DNS,而不是入站规则。
可以在公共仓库上使用自托管 runner 吗?
可以,但 GitHub 不建议这样做。来自 fork 的拉取请求会携带其自身的工作流文件,因此任何可以 fork 您仓库的人,都可以提交在您机器上运行的命令。审批提示只涵盖贡献者的第一次运行。如果您将 runner 连接到公共仓库,请在该 runner 上禁用 fork 拉取请求工作流,不要在该服务器上运行其他内容,并按计划重建这台机器。
为什么注册会因 Http response code: NotFound 失败?
凭据错误时,注册请求也会返回 NotFound,而不仅仅是在 URL 错误时返回,因此该消息容易误导。注册令牌在显示后 1 小时过期,个人访问令牌不能用于此请求。请再次打开 Settings、Actions、Runners、New self-hosted runner,复制新的令牌,并确认 --url 的值指向您拥有管理员权限的仓库。