返回博客

宝塔 chattr +i 锁死 /usr/bin,apt install 永远 Operation not permitted

在某台宝塔面板管理的 Linux 生产机上部署一个需要 Playwright 的服务,跑 npx playwright install-deps chromium 直接炸了:

dpkg: error processing archive .../libnss3.deb (--unpack):
 unable to create '/usr/bin/gpg.dpkg-new': Operation not permitted

root 身份运行,却说"Operation not permitted"——第一反应:文件系统只读?SELinux?检查了半天,最后发现元凶是 chattr +i

现象

lsattr -d /usr/bin /usr/lib/systemd/user
----i---------e-------  /usr/bin
----i---------e-------  /usr/lib/systemd/user

i 属性(immutable)意味着即使 root 也不能在该目录下创建或删除文件。dpkg 在安装任何包时,会先把旧文件备份成 .dpkg-tmp,再写入 .dpkg-new——这两步都需要在目录下创建文件,直接触发 Operation not permitted

更麻烦的是:gnupg / dirmngr 这类包在 apt-get update 时就被半安装了,dpkg 状态变成 broken,后续所有 apt install 都会先要求修复,而修复也会失败——形成死锁。

根因

宝塔/云盾的安全加固不只锁了 /usr/bin 目录本身,还对 /usr/lib/systemd/user//usr/lib/systemd/system/ 下的单个 unit 文件分别加了 chattr +i

坑:只对目录 chattr -i 不够,文件 inode 上有独立的锁
# 只解锁目录,dpkg 仍然报错
chattr -i /usr/bin
apt-get install libnss3   # 还是 Operation not permitted

因为 dpkg 升级 gnupg 时会备份 /usr/lib/systemd/user/gpg-agent-browser.socket,该文件 inode 上也有 +i,目录解锁对它无效。逐一枚举所有锁定文件既不现实,加固工具还可能通过 cron 定期重新加锁。

正解

关键洞察:安装 .so 文件不需要经过 dpkg,只需要:

  1. apt-get download 下载 .deb(只拉文件,不碰 dpkg 状态)
  2. dpkg-deb -x 解压 .deb 到临时目录(纯归档解压,不执行任何脚本,不碰 /usr/bin
  3. .so* 文件复制到 /usr/lib/x86_64-linux-gnu/(该目录通常没有 chattr +i
  4. 跑一次 ldconfig 刷新动态链接缓存
正解:apt-get download + dpkg-deb -x 绕过 dpkg,直接装 .so
# 先确认目标目录可写(这是整个方案的前提)
touch /usr/lib/x86_64-linux-gnu/.test && rm /usr/lib/x86_64-linux-gnu/.test \
  || { echo "目标目录也被锁,方案不适用"; exit 1; }
 
mkdir -p /tmp/debs && cd /tmp/debs
 
# Playwright/Chromium 常见运行时依赖(Ubuntu Jammy 包名)
PKGS="libnss3 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libdrm2 \
      libxkbcommon0 libgtk-3-0 libgbm1 libasound2 libcairo2 \
      libpango-1.0-0 libxdamage1 libharfbuzz0b libthai0 libnspr4 \
      libxcomposite1 libxfixes3 libxrandr2 libx11-6 libdbus-1-3 \
      libcups2 libxcb1 libfontconfig1 libfreetype6 libglib2.0-0"
 
for p in $PKGS; do apt-get download "$p"; done
 
# 解压到临时目录(不走 dpkg install,不动 /usr/bin)
mkdir -p /tmp/extract
for deb in /tmp/debs/*.deb; do dpkg-deb -x "$deb" /tmp/extract; done
 
# 复制 .so(-P 保留符号链接,-n 不覆盖已存在文件)
cp -P -n /tmp/extract/usr/lib/x86_64-linux-gnu/*.so* \
         /usr/lib/x86_64-linux-gnu/
 
ldconfig
rm -rf /tmp/debs /tmp/extract

实测验证

CHROME=$(ls /root/.cache/ms-playwright/chromium-*/chrome-linux*/chrome | head -1)
 
# 1. 没有 missing 依赖
ldd "$CHROME" 2>&1 | grep -c "not found"   # → 0
 
# 2. Chromium 能无头启动
timeout 8 "$CHROME" --headless=new --disable-gpu --no-sandbox \
  --dump-dom about:blank
# 输出 <html><head></head><body></body></html>,退出码 0

几个细节

  • apt-get download 必须在目标主机上执行,不能从其他发行版的容器里 docker cp .so 过来。跨发行版复制会遇到 GLIBC 版本不匹配(比如 Debian Trixie 的 .so 依赖 GLIBC_2.38,而 Ubuntu Jammy 只有 GLIBC_2.35),ldd 还会报 not found
  • dpkg -l libnss3 仍显示"未安装"——因为我们绕过了包管理器。这完全没问题,常规 apt 不会自动覆盖这些文件,除非你显式 apt-get install libnss3
  • 加固工具会重新加锁——如果宝塔/云盾有 cron 任务定期重新 chattr +i,本次操作不受影响,因为我们没有改动任何被锁定路径下的文件。

一句话外卖

apt-get download + dpkg-deb -x + cp 是在 chattr +i 加固主机上安装系统共享库的最小侵入方案——绕过 dpkg 的安装流程,只往可写路径复制 .so,不动任何被锁定的目录。