文章

老Docker构建Bookworm报NO_PUBKEY与内存错误

老版本 Docker 构建 Debian Bookworm 镜像报 NO_PUBKEY 与 lzma 内存错误

一句话结论:不是缺 key,也不是真的内存不足,而是 Jenkins Agent 上的 Docker 太旧,其默认 seccomp 白名单没有放行 Debian 12(Bookworm)新版 glibc 依赖的 clone3 / faccessat2 等 syscall,导致 apt/gpg 验证签名和 xz 解包被”误杀”,报出两个极具迷惑性的假错误。


1. 现象

在 Jenkins Agent 上 docker build 一个基于 python:3.11(Bookworm)的基础镜像时失败,本地机器(Docker 新)一切正常,Jenkins Agent(Docker 旧)必挂。

失败出现在 apt-get update / apt-get install 阶段,报错有两副面孔:

  • 签名类:NO_PUBKEY ... / The following signatures couldn't be verified / InRelease is not signed
  • 解包类:lzma error: Cannot allocate memory + tar: This does not look like a tar archive

2. 弯路:把 apt 命令”修对”没用

一开始怀疑是 apt 命令写错(多条 apt-get 子命令被拼到同一行,导致 --allow-insecure-repositories 被当成 apt-get clean 的参数)。命令确实写错了,但即使把命令改对,错误依旧——因为根因在更底层:syscall 被 seccomp 拦截,apt 这次不是”命令语法”问题,而是”运行时被掐断”。

这也解释了为什么各种 --allow-unauthenticated / trusted=yes 的 workaround 治标不治本。

3. 真正根因

说明
直接原因老 Docker(疑似 < 20.10.10)的默认 seccomp profile 未知 clone3(435)、faccessat2(439)、rseqclose_range 等新 syscall,默认动作 SCMP_ACT_ERRNO 直接返回 EPERM
间接原因宿主 libseccomp2 版本太旧(如 2.3.3),根本”不认识”这些 syscall,seccomp 规则里加了也没用
触发条件基础镜像从 Debian 11(Bullseye)升级到 Debian 12(Bookworm,2023-06 起 python 官方镜像默认切换),其 glibc ≥ 2.34 会主动调用 clone3 / faccessat2
环境差异本地 Docker 新(profile 已含这些 syscall)→ 正常;Jenkins Agent Docker 旧 → 拦截

4. 两个”假错误”的机制

Bookworm 的 apt/gpg 验证签名
  -> 调用 faccessat2 / clone3
  -> 被老 seccomp 拦截,返回 EPERM
  -> 签名验证流程被中断
  => 抛出 "NO_PUBKEY / InRelease is not signed"(误导:让人去折腾 key)

新版 .deb 用 xz 压缩,解包
  -> 依赖被拦的新 syscall;clone3 建线程失败
  -> xz 解压器崩溃 / 内存分配失败
  => 抛出 "lzma error: Cannot allocate memory"(误导:让人去加内存)

两个错误都是同一根因的不同表象,加 key、加内存、改 apt 命令都救不了。

5. 社区印证(不止我们踩坑)

  • Airflow 官方讨论:明确指出 CI 里的 NO_PUBKEY / invalid signature 基本是 red herring,真凶是 runner 上 Docker/libseccomp 太旧,不支持 Bookworm 的 faccessat2/clone3;用 seccomp=unconfined 或升级 Docker 20.10.10+ 即解。(Airflow)
  • UCS Bug 55360:glibc ≥ 2.34 用 clone3+faccessat2,旧 Docker(19.03.8) 默认 seccomp 不放行,libseccomp2 2.3.3 不认识这些 syscall;最终修复是升级 libseccomp2 ≥ 2.5 并在 seccomp 规则加 clone3 / rseq / faccessat2。(UCS)
  • crazylion 博客:Docker ≤ 20.10.9 跑新版 glibc,apt 安装包报 lzma error: Cannot allocate memory / tar: This does not look like a tar archive——与本问题 100% 吻合,根因是 clone3 被 seccomp 拦截。(crazylion)
  • Alpine 3.14 发布说明scmp_sys_resolver faccessat2 返回 439 = 支持,-1 = 不支持,可快速验证宿主 libseccomp 是否认得该 syscall。(Alpine)
  • devopsaitoolkit:多篇文章确认 clone3 被拦会表现为 pthread_create failed: Operation not permitted / apt 签名失败,并强调 seccomp=unconfined 只是临时诊断手段。(devops)

6. 解决方案

方法 1:构建时关闭 seccomp(最快,临时止血)

改 CI 里的 docker build

docker build --security-opt seccomp=unconfined \
             --memory=2g --memory-swap=2g \
             -t reg.qschou.com/qsevidence/$app_id:${env}_${BranchOrTag} .

⚠️ seccomp=unconfined完全关闭 syscall 过滤、削弱容器隔离,只应作为临时诊断/止血,不要进生产。配合的 --memory/--memory-swap 对”假内存错误”也对症。

方法 1 增强:自定义 seccomp profile(推荐止血方案)

unconfined 更安全——只放行缺失的 syscall,保留其余隔离:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86"],
  "syscalls": [
    {
      "names": ["clone3", "faccessat2", "rseq", "close_range"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

保存为 seccomp-bookworm.json,然后:

docker build --security-opt seccomp=seccomp-bookworm.json \
             -t reg.qschou.com/qsevidence/$app_id:${env}_${BranchOrTag} .

方法 2:Dockerfile 强制 trusted 源(仅辅助,非根因修复)

FROM reg.qschou.com/base/python:3.11-tectonic

RUN find /etc/apt -type f \( -name "*.list" -o -name "*.sources" \) -exec \
    sed -i -e 's/^deb /deb [trusted=yes] /' \
           -e 's/^Types: deb/Types: deb\nTrusted: yes/' {} + || true

RUN apt-get update -o Acquire::AllowInsecureRepositories=true \
                   -o APT::Get::AllowUnauthenticated=true && \
    apt-get install -y --no-install-recommends --allow-unauthenticated \
        ca-certificates fonts-noto-cjk fontconfig && \
    fc-cache -fv && \
    rm -rf /var/lib/apt/lists/*

这一步让 apt 不再校验签名,“假 NO_PUBKEY”不报了,但底层 syscall 被拦没解决,涉及 xz 解包/其他依赖新 syscall 的操作可能换个错继续挂。只当配合手段。

方法 3:升级 Jenkins Agent 的 Docker(根本解决)

要求运维把 Docker 升到 24+ 或至少 20.10.10 以上,并同步升级 runc / containerd / libseccomp2(≥ 2.5)。这才是根因修复。

# 在 Agent 上验证/升级
docker version --format '{{.Server.Version}}'
sudo apt-get install --only-upgrade docker-ce docker-ce-cli containerd.io libseccomp2
sudo systemctl restart docker

7. 先确认”是不是 seccomp”的诊断命令

在 Jenkins Agent 上跑(不改任何东西):

# 看 Docker / runc / libseccomp 版本
docker version --format '{{.Server.Version}}'; runc --version

# 验证宿主 libseccomp 是否认得 faccessat2(439=支持, -1=不支持)
scmp_sys_resolver faccessat2

# 关键对照实验:unconfined 能跑通就坐实是 seccomp 拦截
docker run --rm --security-opt seccomp=unconfined <你那个失败的构建步>

第 3 步在 unconfined 下能跑通 → 100% 坐实 seccomp 拦截,方法 1 / 增强方案可放心用。

8. 结论与处置顺序

  1. 先试方法 1(或方法 1 增强) 验证并止血,把新日志确认通过;
  2. 同时把方法 2 当辅助,保证 apt 侧不再因签名校验卡住;
  3. 推动运维走方法 3 升级 Docker / libseccomp2,这是唯一的根因修复,避免长期依赖 unconfinedtrusted=yes 削弱安全性。

当前结论

Jenkins Agent 上 Docker 过旧,其默认 seccomp 白名单未放行 Debian 12(Bookworm)新版 glibc 依赖的 clone3 / faccessat2 等 syscall,导致 apt/gpg 签名验证与 xz 解包被拦截,分别伪装成 NO_PUBKEY 公钥错误和 lzma: Cannot allocate memory 内存错误。本地 Docker 新故正常。

建议处理

  • 临时:方法 1 增强(自定义 seccomp profile 放行缺失 syscall)止血,避免直接用 unconfined
  • 辅助:方法 2 Dockerfile 强制 trusted 源,确保 apt 侧不卡签名;
  • 根本:方法 3 升级 Agent 的 Docker ≥ 20.10.10(建议 24+)并升级 libseccomp2 ≥ 2.5。