TT Lab
开始
学习 学习路径 课程

隔离网络镜像与私有 CA

pip 分成两台电脑

在 TT Lab 中继续学习

一句话总结

隔离网络中的 pip 也分成两台计算机。下载端从互联网收集 wheel 并写下哈希,使用端把这些 wheel 搭建成内部索引,并通过 pip.conf 中的一行指向它。连接两者的只有文件包和哈希清单。

为什么需要它

在分析服务器上执行 pip install requests,隔离网络中会出现 Could not find a version that satisfies the requirement。从外部只带回一个 requests wheel,这次又会提示缺少 urllib3、idna、certifi 和 charset_normalizer。对于导入评审每天只有一次的组织,这样往返一次就是一天。而且,如果下载用的计算机(笔记本,Python 3.12)与使用的服务器(Python 3.11)不同,带回来的 wheel 中已编译的部分在服务器上就不匹配。在这个实验镜像中下载 requests 2.32.3,会发现只有 charset_normalizer 是带有 cp312-cp312-manylinux...x86_64 标签的平台 wheel,其余四个都是 py3-none-any。Python 版本一变,恰好就是那一个出问题。

工作原理

下载端。 pip download -d <디렉터리> <요구사항>(占位符依次为目录和依赖声明)不安装,而是解析依赖后只收集文件。目标服务器不同时,用 --platform、--python-version、--implementation、--abi 来写明目标。pip 文档写明,使用这些选项时必须同时使用 --only-binary=:all: 或 --no-deps。因为如果下载源码分发包并在当前计算机上构建,就会与目标服务器不一致。

固定版本和哈希。 pip hash <파일>(占位符为文件名)会输出一行 --hash=sha256:<값>(占位符为哈希值)。在 requirements.txt 的每一行写上 이름==버전 --hash=sha256:...(占位符依次为软件包名和版本),正如 pip 文档所说,只要有任何一条需求带有 --hash,哈希检查模式就会对全部需求开启。在这个模式下,所有需求都必须用 == 固定,只要有一个下载文件的哈希不同,安装就会停止。这就堵住了在导入介质传递过程中被改动的 wheel 悄悄进入的通道。

使用端:内部索引。 pip 读取的索引,是 PEP 503(现在是 packaging.python.org 的 Simple repository API 规范)规定的简单 HTML。只要 /simple/<정규화한 이름>/(占位符为规范化后的名称)下有以文件名为文字的链接就行。名称规范化是先转为小写,再把连续的 .、-、_ 替换成一个 -。所以 charset_normalizer wheel 必须位于 /simple/charset-normalizer/ 才能被找到。Python 标准库的 http.server 会以恰好这种形式(文件名为文字的链接)提供目录列表,所以只要把目录建成规范化后的名称并放入 wheel,就成了只读索引。pypiserver 这类工具所做的事,骨架也是这个。

指向:pip.conf。 在 Linux 上,pip 按全局(/etc/xdg/pip/pip.conf、/etc/pip.conf)→ 用户(~/.config/pip/pip.conf,旧位置 ~/.pip/pip.conf)→ 站点($VIRTUAL_ENV/pip.conf)→ PIP_CONFIG_FILE 的顺序读取配置,后读取的值覆盖前面的。环境变量优先于文件,命令行选项优先于环境变量。pip config debug 会显示实际读取了哪些文件。在全局文件中写入 index-url,服务器上所有 venv 都会使用内部索引。

连索引都没有时。 --no-index --find-links <디렉터리>(占位符为目录)会完全忽略索引,只看该目录中的文件。这是直接使用用 USB 带进来的 wheelhouse 的办法,同时也是检验 wheelhouse 本身是否完整的测试。

在现场相遇的样子

如果用 http:// 搭建内部索引,并且只写 index-url,pip 就不会信任该地址。因为 localhost 和 127.0.0.1 被视为安全,但内部名称不是。在本实验中用 pypi.airgap.internal 测试,会发现一去掉 trusted-host,pip 就只留下警告并忽略该索引,结果是 No matching distribution found。看上去像是索引是空的,其实是根本没有去看。实务中,正确的做法是用 HTTPS 搭建内部索引,并让系统信任私有 CA(后面的模块会讲)。

第二种常见事故是 PEP 668。Ubuntu 24.04 的系统 Python 带有 EXTERNALLY-MANAGED 标记,会拒绝 venv 之外的 pip install。不要用 --break-system-packages 硬装,而是创建 venv。全局的 /etc/pip.conf 在 venv 内的 pip 也会读取,所以配置一次就够了。

再说一下证书。据 pip 文档,从 pip 24.2 起,在 Python 3.10 及以上版本中,会通过 truststore 同时使用操作系统证书库,在此之前只使用 certifi 包。本实验镜像中的 pip 是 Ubuntu 软件包 24.0,它是 Ubuntu 修改过的版本,会读取 /etc/ssl/certs/ca-certificates.crt(实测)。即使同样是 24.0,从 PyPI 下载的 pip 行为也可能不同,所以在使用私有 CA 的地方,最好通过 cert 设置或 PIP_CERT 明确指定证书包。

下一项实验要做什么

分别为这个 Pod 的 Python 和隔离网络服务器的 Python 3.11 下载 requests 2.32.3,并制作写入了哈希的 requirements.txt。在 /srv/pypi/simple 下用规范化的目录搭建索引,以 pypi.airgap.internal:8080 启动,并用 /etc/pip.conf 指向它,然后由评分器确认在外部被封闭的状态下是否也能下载。最后,不用索引,只靠 wheelhouse 完成安装。

参考文档:pip download、pip Configuration、Secure installs(哈希检查模式)、Simple repository API、HTTPS Certificates