- 包管理器
- 开发工具
- CLI
【免费下载链接】pnpm
Fast, disk space efficient package manager
导读
本指南围绕 pnpr(pnpm 仓库自带的 OCI 镜像分发服务)新增的oci.bearerAuth配置项展开,讲解如何在 pnpr 的 OCI Distribution 接口上启用 Bearer 令牌认证,让docker、podman、skopeo等容器客户端通过短时、仓库级作用域(repository-scoped)的凭证访问私有镜像。读完本文,你将掌握该配置的启用方式、令牌的签发与校验流程、权限授予规则、上游 pull-through 场景下的令牌协商机制,以及相关的安全边界与测试验证方法。
一、背景:pnpr 的 OCI 分发面与 Bearer 挑战
pnpr 在pnpr/crates/pnpr/src/server/oci/tokens.rs中实现了一套完整的 OCI token 服务。pnpr 的 OCI 服务面暴露/v2/下的 OCI Distribution API,并同时支持/oci/v2/与/oci/~<name>/v2/两种寻址方式(见 pnpr README)。
OCI 生态的认证采用Bearer 挑战-响应(challenge-response)模式:当客户端(如docker、podman、skopeo)访问受保护的资源且未携带有效凭证时,服务端返回401 Unauthorized,并在WWW-Authenticate响应头中给出形如Bearer realm="...",service="..."的挑战;客户端随后携带自己的身份凭证向realm指向的 token 服务换取一个访问令牌,再用该令牌访问真正的资源。
在启用该特性之前,pnpr 的 OCI 面不具备这套令牌协商能力,容器客户端无法使用短时、仓库级作用域的凭证。本特性对应的 changeset 记录在 .changeset/pnpr-oci-scoped-tokens.md 中:设置oci.bearerAuth: true即可让容器客户端使用短时、仓库级作用域的凭证。
二、启用oci.bearerAuth
oci.bearerAuth是 pnpr 配置中oci区块的布尔开关,与镜像字节数限制等参数并列定义。从源码看,其结构体定义在 pnpr/crates/config/src/lib.rs 的OciConfig中:
/// Authentication and byte limits for the OCI distribution surface. #[derive(Debug, Clone, Deserialize)] #[serde(default, rename_all = "camelCase", deny_unknown_fields)] pub struct OciConfig { pub bearer_auth: bool, pub max_blob_bytes: u64, pub max_manifest_bytes: usize, } impl Default for OciConfig { fn default() -> Self { Self { bearer_auth: false, max_blob_bytes: 10 * 1024 * 1024 * 1024, max_manifest_bytes: 4 * 1024 * 1024, } } }要点:
- 字段采用 camelCase 序列化(
rename_all = "camelCase"),因此配置文件中必须写作oci.bearerAuth,而不是oci.bearer_auth; - 默认值为
false,即默认不启用 Bearer 认证,与既有行为保持兼容; - 配置结构使用
deny_unknown_fields,写入oci区块中未知的字段会导致配置校验失败。
在 pnpr 的 YAML 配置文件中启用方式如下:
oci: bearerAuth: true # 以下为同区块的既有配置,可一并调整 maxBlobBytes: 10737418240 # 单层 blob 上限,默认 10 GiB maxManifestBytes: 4194304 # 单 manifest 上限,默认 4 MiB在 pnpr README 中,对启用后的行为有明确说明:
Set
oci.bearerAuth: trueto offer a Bearer challenge. Clients exchange their pnpr credential at/v2/tokenfor a five-minute token scoped to repositorypull,push, ordeleteactions. The endpoint grants only permitted actions. The parent credential's revocation, read-only flag, and network restrictions remain effective. Anonymous tokens can pull public repositories. Use the samesecret:and registry configuration on all replicas so their tokens interoperate. Scoped tokens cannot access other pnpr APIs or the whole catalog.
三、挑战(challenge)的生成
当oci.bearerAuth: true时,任何对 OCI 资源的未授权访问都会得到一个规范的 Bearer 挑战。该逻辑位于challenge()函数(pnpr/crates/pnpr/src/server/oci/tokens.rs):
let realm = format!("{}{base}/token", state.inner.config.http.public_url.trim_end_matches('/')); let mut value = format!(r#"Bearer realm="{realm}",service="pnpr""#);即服务端生成的挑战格式为:
WWW-Authenticate: Bearer realm="https://<pnpr 公网地址>/<base>/token",service="pnpr",scope="repository:<name>:<actions>"几点实现细节:
realm指向 token 签发端点/v2/token(或对应生态前缀下的 token 端点);service固定为"pnpr",token 签发时也会校验查询参数中的service必须等于pnpr,否则返回BadRequest(“invalid OCI token service”);scope依据请求方法动态推导:GET/HEAD请求对应pull,DELETE对应pull,push,delete,其余写操作对应pull,push;- 挑战响应会经过
private_no_cache处理,禁止缓存,避免令牌/挑战信息被中间层复用。
如果oci.bearerAuth未开启,则即使收到401,响应中也不会附加WWW-Authenticate头,客户端不会进入令牌协商流程。
四、令牌的签发:/v2/token端点
客户端收到挑战后,携带自己的 pnpr 身份凭证(如Authorization: Basic ...)向realm端点发起令牌请求。issue_token()的完整处理流程(pnpr/crates/pnpr/src/server/oci/tokens.rs)如下:
- 开关检查:若
oci.bearerAuth为false,直接返回Unsupported错误(“OCI Bearer authentication is disabled”); - 目标注册表解析:通过
addressed_registry()解析请求寻址的 OCI 注册表; - 父凭证校验:从请求头解析身份凭证(
token_credentials);若请求携带了凭证但调用方身份为匿名,返回Unauthorized(“invalid credentials”); - 父令牌追溯:若携带的是已签发的 pnpr 令牌,则按其哈希查询父令牌记录,并继承其
readonly(只读)标记; - 作用域授予:根据请求查询参数中的
scope计算调用方实际被授予的仓库作用域(见下一节); - 签名签发:将 claims(父令牌引用、audience、过期时间、作用域映射)序列化后,用 P-256 ECDSA 私钥签名,输出形如
pnpr_oci_<payload>.<signature>的令牌(TOKEN_PREFIX = "pnpr_oci_"),并以 JSON 返回:
{ "token": "pnpr_oci_<payload>.<signature>", "expires_in": 300 }4.1 短时 TTL 与客户端侧缓存
令牌的 TTL 由常量TTL: u64 = 300决定(5 分钟),每次签发都会返回expires_in: 300。从源码注释可以确认,签发方同时维护了令牌过期时间的取整与安全余量策略:cache_oci_token中expires_in会被限制在5..=3600秒,且缓存时预留 5 秒的提前过期余量,避免边缘场景下使用即将过期的令牌。
4.2 签名与防伪造
令牌签名使用p256::ecdsa::SigningKey,其私钥由resolution_cache_secret派生(sha256("pnpr OCI token signing v1\0" + secret))。校验端(decode/verify_claims)会:
- 校验令牌前缀
pnpr_oci_; - 拒绝长度超过
16 * 1024字节的令牌; - 用 URL-safe Base64 解码 payload 与签名,使用 P-256 公钥验签;
- 校验
expires是否已过期; - 校验
oci.bearerAuth是否仍处于开启状态(关闭后旧令牌立即失效)。
由于私钥由secret:配置派生,README 特别强调:所有 replica 应使用相同的secret:与注册表配置,令牌才能互相校验通过。
五、仓库级作用域:只能拿到“被授权的最小权限”
这是本特性最核心的设计:令牌携带的不是全局权限,而是一个仓库名到动作集合的映射。令牌 claims 结构如下:
pub struct Claims { pub parent: Option<String>, // 父令牌的哈希引用(用于追溯与撤销) audience: String, // 令牌的 audience(即寻址路径) expires: u64, // 过期时间戳(秒) scopes: BTreeMap<String, Vec<String>>, // 仓库名 -> 动作列表 }5.1 scope 解析与授予
令牌请求的scope参数遵循 OCI 标准格式repository:<name>:<actions>(动作可逗号分隔多个,如pull,push)。granted_scopes与grant_one_scope的处理逻辑:
- 未知资源类型直接忽略:
resource不是repository或repository(plugin)的 scope 会被跳过——这保证 pnpr 自己的内部 scope 不会被错误授予; - 非法 scope 被丢弃而非拒绝整个请求(与 OCI 协议一致:对不可读仓库的访问以“空授予”应答,而非直接报错);
- 仓库名必须能解析为合法的 OCI 包名(
CanonicalPackageName::parse),否则忽略; - 单个令牌最多授予 32 个不同仓库,超出返回
BadRequest(“too many OCI token scopes”); - 每个动作必须通过两次授权:
authorize(...Action::Access)(可读)是任何动作的前提,之后pull对应Action::Access、push对应Action::Publish、delete对应Action::Unpublish; - 只读父令牌的约束:若父令牌携带
readonly标记,则非pull动作一律不被授予。
5.2 令牌的使用校验
客户端拿到令牌后访问GET /v2/<name>/manifests/...等资源时,服务端调用Claims::permits(path, method)校验:
- 令牌的
audience必须与请求路径前缀匹配,且后续路径必须形如/v2/...; - 从路径中解析出端点(manifest、blob、tags、referrers、upload 等),并提取仓库名;
- 依据方法推导所需动作:
GET/HEAD→pull,DELETE→delete,上传会话(含取消)→push; - 只有请求的仓库确实在
scopes中且包含所需动作时才放行。
测试用例token_scopes_ignore_unknown_resources_and_count_distinct_repositories直接验证了这些行为:构造携带多个scope的令牌请求,确认未知资源被忽略、不同仓库被分别计数,且返回的令牌可以按 Bearer 方式用于后续请求。
5.3 令牌边界:不能做什么
README 明确强调令牌的边界:
- 令牌无法访问 pnpr 的其他 API(如 npm 生态接口);
- 令牌无法枚举整个 catalog(
Endpoint::Catalog在permits中直接返回false); - 令牌无法扩展到未授予的仓库;
- 父凭证的撤销、只读标记与网络限制始终生效——即如果父凭证被吊销,其后代令牌即使未过期也无法继续使用(
decode校验后还需通过authorize阶段)。
六、上游 pull-through:镜像仓库作为上游时的令牌协商
oci.bearerAuth不仅作用于 hosted(托管)镜像仓库,也深刻影响pull-through 代理场景。pnpr 可以将 Docker Hub、GHCR 等配置为 OCI 上游(见 pnpr README 中的示例):
registries: dockerhub: type: upstream ecosystem: oci url: https://registry-1.docker.io/ public: true defaultRegistry: dockerhub上游 OCI 拉取的核心实现在 pnpr/crates/upstream/src/oci.rs,其协商流程:
- 无凭证首访:pnpr 先不带令牌发起
GET/HEAD,若收到401且WWW-Authenticate是Bearer挑战,则解析挑战; - 解析挑战:仅接受
Bearerscheme,且realm/service参数合法(不支持反斜杠转义); - 校验 realm:
token_realm_allowed要求 realm 无用户名/密码/片段,且要么与注册表同源,要么是 Docker Hub →https://auth.docker.io/token这一被明确允许的组合; - 换取令牌:向 realm 发起带
service与scope=repository:<name>:pull的请求,凭证(Authorization头)只会在“realm 与注册表不同源但传输安全”时转发; - 缓存令牌:按仓库缓存,
expires_in截断到5..=3600秒并预留 5 秒余量,最多缓存 256 个条目,过期条目惰性清理; - 重放与重定向:携带令牌重试请求;对层下载的重定向(如 Docker Hub 的 Cloudflare CDN),仅在
oci_download_allowed白名单内的目标(Docker Hub 的三个官方 CDN 源、GHCR 的pkg-containers.githubusercontent.com)才放行。
源码注释明确了两条安全红线:
- 凭证绝不流向层 CDN 或重定向后的 token 端点(
with_oci_credentials仅在 URL 与注册表同源且传输安全时才附加凭证); - 下游请求者(容器客户端)的凭证也不会被透传给上游——令牌只在 pnpr 与上游之间协商,客户端只会拿到 pnpr 自己签发的 scoped token。
七、安全模型与运维要点
综合源码与 README,使用oci.bearerAuth时需要关注以下安全与运维边界:
| 关注点 | 说明 | 依据 |
|---|---|---|
| 默认关闭 | bearer_auth: false,显式开启后才提供 Bearer 挑战 | OciConfig::default |
| 短时令牌 | TTL 固定 5 分钟,过期即失效 | TTL: u64 = 300 |
| 仓库级作用域 | 令牌仅含被授予仓库的pull/push/delete动作 | Claims.scopes |
| 匿名可拉公共仓库 | 匿名令牌可执行pull公共仓库,不可执行写操作 | grant_action逻辑 |
| 父凭证撤销生效 | 父凭证被吊销或只读,后代令牌自动受限 | parent追溯 |
| 多副本一致性 | 所有 replica 需共享同一secret:与注册表配置 | pnpr README |
| 令牌不越权 | 无法访问其他 pnpr API、无法枚举 catalog | permits()拒绝Endpoint::Catalog |
| 上游凭证不外泄 | 上游协商的凭证不流向层 CDN 与重定向目标 | oci.rs |
八、如何验证:测试与实操
8.1 仓库内的自动化验证
pnpr 为 OCI 认证提供了完整的集成测试,是理解该特性的最佳入口:
- pnpr/crates/pnpr/tests/oci_registry/authorization.rs:覆盖令牌签发、作用域忽略/计数、Bearer 携带请求等核心路径,测试中显式设置
config.http.oci.bearer_auth = true; - pnpr/crates/pnpr/tests/oci_registry/uploads.rs:覆盖携带认证的 blob 上传会话;
- pnpr/crates/pnpr/tests/oci_registry/behavior.rs:OCI 面的行为级测试;
- pnpr/crates/config/src/tests/upstream.rs:验证上游
auth: type: bearer会转换为Authorization: Bearer <token>头,以及oci.bearerAuth配置的解析。
8.2 手动验证
启用oci.bearerAuth: true后,可用任一容器客户端验证完整协商流程:
# 1. 未携带凭证访问,观察 401 + WWW-Authenticate 挑战头 curl -i https://pnpr.example.com/v2/acme/app/manifests/latest # 2. 携带 pnpr 身份凭证向 realm 换取 scoped token # (realm 由上一步的 WWW-Authenticate 头给出,如 .../v2/token?service=pnpr&scope=repository:acme/app:pull) # 3. 使用 skopeo 完整走一遍 Bearer 协商 skopeo copy docker://pnpr.example.com/acme/app:1.0 oci:./app:1.0skopeo的等价命令亦见 pnpr README 的 OCI 章节,podman、docker等标准客户端遵循同一 Bearer 流程,无需额外配置即可对接。
九、结语
oci.bearerAuth为 pnpr 的 OCI 分发面补上了标准的 Bearer 令牌认证能力:客户端用一次性、5 分钟有效、仓库级作用域(pull/push/delete)的令牌访问镜像资源,令牌由 P-256 签名防伪造,父凭证的撤销与只读语义全程生效,上游 pull-through 场景下凭证也严格限定在注册表与可信 realm 之间,绝不流向层 CDN。该特性在 .changeset/pnpr-oci-scoped-tokens.md 中标记为@pnpm/pnpr的 minor 变更,源码、配置与集成测试均位于pnpr/目录,可作为深入研读的起点。
- 包管理器
- 开发工具
- CLI
【免费下载链接】pnpm
Fast, disk space efficient package manager
相关推荐
cosign login 命令完全指南:向 OCI 镜像仓库进行身份认证
cosign login 命令完全指南:向 OCI 镜像仓库进行身份认证 导读 cosign login 是 cosign( README.md https:/
供应链安全云原生应用安全pnpm 仓库生态下的 pnpr OIDC 认证指南:浏览器登录、subject 映射与 GitHub Actions 免令牌发布
pnpm 仓库生态下的 pnpr OIDC 认证指南:浏览器登录、subject 映射与 GitHub Actions 免令牌发布 导读 pnpr 是 pnpm
包管理器开发工具CLIArgo CD `argocd repo add` 命令完全指南:Git/OCI/Helm 仓库接入与多认证方案实战
Argo CD argocd repo add 命令完全指南:Git/OCI/Helm 仓库接入与多认证方案实战 argocd repo add 是 Argo
云原生CI/CD容器编排DevOps后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考