ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

kOps 中的容器镜像认证机制:go-containerregistry `authn` 包源码级解析与实践指南

kOps 中的容器镜像认证机制:go-containerregistry `authn` 包源码级解析与实践指南 kOps 中的容器镜像认证机制go-containerregistryauthn包源码级解析与实践指南【免费下载链接】kopsKubernetes Operations (kOps) - Production Grade k8s Installation, Upgrades and Management项目地址: https://gitcode.com/gh_mirrors/kop/kops导读在 kOpsKubernetes Operations中从私有仓库拉取组件镜像、在资产镜像仓库间拷贝镜像等场景都依赖对容器仓库的认证能力。本文以 kOps 仓库内 vendored 的vendor/github.com/google/go-containerregistry/pkg/authn包文档为主线完整讲解Keychain/Authenticator两大核心接口、DefaultKeychain对 Docker/Podman 配置的解析顺序、凭据助手credential helper协议以及仓库侧 token 与 OAuth2 两种认证流程并结合remote.WithAuthFromKeychain与 kOps 实际调用代码如pkg/assets/assetcopy/copyimage.go给出可直接落地的 Go 实战示例。读完本文你将能够在 kOps 相关工具中正确复用 Docker 登录凭据、为 ECR/ACR/GCR 等云仓库接入凭据助手、通过NewMultiKeychain组合多来源凭据并能独立调试凭据不生效的疑难问题。一、背景为什么 kOps 需要一套统一的镜像认证机制kOps 在构建集群过程中需要与多个容器镜像仓库打交道Kubernetes 组件镜像、etcd、DNS 控制器等默认从公共仓库拉取而生产环境尤其是高安全要求集群常把镜像镜像到自建仓库后再分发。无论是直连公共仓库读取公开镜像还是在私有仓库之间拷贝镜像第一步都绕不开认证。kOps 选择在vendor/目录下携带 go-containerregistry正是因为它提供了一套与dockerCLI 行为高度一致的认证抽象。其设计哲学在原 README 中写得很明确尽可能模拟docker的认证行为与配置方式使已配置好docker凭据的用户可以开箱即用而当配置失效时对内部机制的基本理解有助于调试。该包包含 8 个源文件职责划分清晰文件职责authn.goAuthenticator/ContextAuthenticator接口与AuthConfig结构体、base64 编解码keychain.goKeychain接口、DefaultKeychain实现、Helper适配、刷新型 keychainmultikeychain.goNewMultiKeychain多 keychain 组合anon.go匿名认证单例Anonymousbasic.goBasic 认证Basic{Username, Password}bearer.goBearer 认证Bearer{Token}auth.goFromConfig包装器二、核心抽象Authenticator 与 Keychain2.1Authenticator一次认证的产出物在 authn.go 中Authenticator是唯一的凭证提供者接口// Authenticator is used to authenticate Docker transports. type Authenticator interface { // Authorization returns the value to use in an http transports Authorization header. Authorization() (*AuthConfig, error) }它返回的AuthConfig结构体authn.go#L47-L60内联了github.com/docker/cli/cli/config/types中用到的字段type AuthConfig struct { Username string json:username,omitempty Password string json:password,omitempty Auth string json:auth,omitempty // IdentityToken is used to authenticate the user and get // an access token for the registry. IdentityToken string json:identitytoken,omitempty // RegistryToken is a bearer token to be sent to a registry RegistryToken string json:registrytoken,omitempty }四个字段的分工对应四种凭据形态Username/PasswordBasic 用户名密码、Authbase64 编码的username:password见下文 Docker Config 章节、IdentityTokenOAuth2 身份令牌、RegistryToken已获取的 Bearer 令牌。包内提供了若干现成实现Anonymousanon.go单例匿名认证器Authorization()直接返回空AuthConfig{}即不携带任何凭据Basicbasic.go携带用户名/密码Bearerbearer.go携带现成的 registry token填入RegistryToken字段FromConfigauth.go把任意AuthConfig包装为Authenticator。此外authn.go还定义了带context的变体接口与统一入口type ContextAuthenticator interface { AuthorizationContext(context.Context) (*AuthConfig, error) } // Authorization calls AuthorizationContext with ctx if the given Authenticator // implements ContextAuthenticator, otherwise it calls Resolve with the given Resource. func Authorization(ctx context.Context, authn Authenticator) (*AuthConfig, error)从源码结构可以看出设计意图优先使用上下文感知的实现便于传递超时、trace 等否则退化为无上下文的传统调用。2.2Keychain按镜像引用解析凭据单个Authenticator只解决这一份凭据如何呈现而针对某个仓库该用哪份凭据由Keychain负责。接口定义在 keychain.go// Resource represents a registry or repository that can be authenticated against. type Resource interface { String() string // e.g. gcr.io/my-project 或 gcr.io RegistryStr() string // 仅返回 registry 部分例如 gcr.io } // Keychain is an interface for resolving an image reference to a credential. type Keychain interface { Resolve(Resource) (Authenticator, error) }其中Resource由pkg/name中的引用类型如name.ParseReference的结果实现RegistryStr()提取主机名用于匹配配置String()提供完整引用用于精确匹配。三、DefaultKeychain模拟 Docker 凭据解析的默认实现DefaultKeychainkeychain.go#L59-L62是使用最频繁的入口。它的完整解析流程可以从ResolveContextkeychain.go#L86-L186的源码中逐段还原第 1 步定位 Docker 配置文件优先检查$HOME/.docker/config.jsonWindows 为%USERPROFILE%\.docker\config.json未找到则检查$DOCKER_CONFIG/config.json若设置了DOCKER_CONFIG环境变量两者都未找到时回退到$REGISTRY_AUTH_FILE指向的文件再回退到 Podman 的认证文件$XDG_RUNTIME_DIR/containers/auth.json若未设置XDG_RUNTIME_DIR或文件不存在则尝试$XDG_CONFIG_HOME/containers/auth.jsonXDG_CONFIG_HOME未设置时默认$HOME/.config全部落空则返回Anonymous匿名访问。第 2 步按引用逐级匹配for _, key : range []string{ target.String(), target.RegistryStr(), } { if key name.DefaultRegistry { key DefaultAuthKey } cfg, err cf.GetAuthConfig(key) ... }匹配键依次尝试完整引用 → registry 主机名命中即用。值得注意的是常量DefaultAuthKeykeychain.go#L64-L68// DefaultAuthKey is the key used for dockerhub in config files, which // is hardcoded for historical reasons. DefaultAuthKey https:// name.DefaultRegistry /v1/即 Docker Hub 在配置文件中的历史固定键https://index.docker.io/v1/——这正是docker login写入 config.json 时使用的键名模拟此行为可确保 Docker Hub 凭据兼容。第 3 步兜底策略若某级解析结果为空AuthConfig继续尝试下一级全部为空则返回Anonymous。整个过程的注释中引用了 google/ko 的 issue #90 与 moby 的registry/config.go说明该实现刻意对齐 Docker 引擎自身的GetAuthConfig语义。3.1 与 Docker CLI 的解析委托DefaultKeychain在找到配置文件后调用的是github.com/docker/cli/cli/config.Load及针对$REGISTRY_AUTH_FILE/Podman 路径的LoadFromReader来解析文件并触发必要的凭据助手然后借用其ConfigFile.GetAuthConfig完成配置 registry 域名 →AuthConfig的换算从而复用 Docker CLI 中全部历史兼容逻辑。这也是只要docker login能用DefaultKeychain就能用的根基。四、给包使用者的快速上手tl;dr原 README 给出的最小示例是理解authn包的最佳起点默认情况下pkg/v1/remote使用Anonymous即无凭据对大多数仓库只能读取公共镜像要使用 Docker config 中的凭据只需显式传入authn.DefaultKeychainpackage main import ( fmt github.com/google/go-containerregistry/pkg/authn github.com/google/go-containerregistry/pkg/name github.com/google/go-containerregistry/pkg/v1/remote ) func main() { ref, err : name.ParseReference(registry.example.com/private/repo) if err ! nil { panic(err) } // Fetch the manifest using default credentials. img, err : remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) if err ! nil { panic(err) } // Prints the digest of registry.example.com/private/repo fmt.Println(img.Digest) }关键点remote.WithAuthFromKeychain(keys authn.Keychain)是remote包提供的函数式选项options.go#L214-L221。remote包内部的选项校验逻辑options.go#L154-L159明确规定WithAuth与WithAuthFromKeychain不能同时使用否则会返回错误 provide an option for either authn.Authenticator or authn.Keychain, not both这是为了避免用户同时指定两套认证而静默失效。4.1 kOps 中的真实调用资产镜像拷贝kOps 的资产assets管理模块就是该模式的落地案例。pkg/assets/assetcopy/copyimage.go 中的CopyImage.Run()负责把镜像从源仓库拷贝到目标仓库典型场景是高安全集群的内部镜像仓库options : []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain)} desc, err : remote.Get(sourceRef, options...) if err ! nil { return fmt.Errorf(fetching %q: %v, source, err) } targetDesc, err : remote.Get(targetRef, options...) if err nil desc.Digest.String() targetDesc.Digest.String() { klog.Infof(no need to copy image from %v to %v, sourceRef, targetRef) return nil } switch desc.MediaType { case types.OCIImageIndex, types.DockerManifestList: // Handle indexes separately. if err : copyIndex(desc, sourceRef, targetRef, options...); err ! nil { return fmt.Errorf(failed to copy index: %v, err) } default: // Assume anything else is an image, since some registries dont set mediaTypes properly. if err : copyImage(desc, sourceRef, targetRef, options...); err ! nil { return fmt.Errorf(failed to copy image: %v, err) } }这段代码体现了三个实践要点复用DefaultKeychain源/目标仓库的凭据都从 Docker config或 Podman auth 文件解析无需硬编码密钥幂等跳过若目标仓库已有相同 digest 的镜像则直接跳过避免重复拷贝按 MediaType 分流manifest list/OCI index 走copyIndex其余按普通镜像处理注释还说明部分仓库 MediaType 设置不规范因此默认按镜像处理。当你通过kops toolbox或相关 asset 命令配置镜像仓库镜像时背后依赖的正是authn包这套机制。五、模拟云厂商凭据助手ECR / ACR / GCR5.1NewKeychainFromHelper把凭据助手实现变成 KeychainDocker 生态的凭据助手credential helper通常是独立可执行文件如docker-credential-ecr-login。authn包提供了 NewKeychainFromHelperNewKeychainFromHelper(h Helper) Keychain允许你在 Go 进程内直接嵌入某个凭据助手的实现而无需它在$PATH上存在。Helper是github.com/docker/docker-credential-helpers/credentials.Helper接口的子集只需实现一个方法type Helper interface { Get(serverURL string) (string, string, error) }其内部wrapper.ResolveContextkeychain.go#L214-L225有一个容易被忽略的细节当返回的用户名为字面量token时会把 Secret 当作IdentityToken而非 Password 处理与 Docker 凭据助手协议中的约定一致。模拟 Amazon ECR 凭据助手docker-credential-ecr-loginimport ( ecr github.com/awslabs/amazon-ecr-credential-helper/ecr-login github.com/awslabs/amazon-ecr-credential-helper/ecr-login/api github.com/google/go-containerregistry/pkg/authn github.com/google/go-containerregistry/pkg/v1/remote ) func main() { // ... ecrHelper : ecr.ECRHelper{ClientFactory: api.DefaultClientFactory{}} img, err : remote.Get(ref, remote.WithAuthFromKeychain(authn.NewKeychainFromHelper(ecrHelper))) if err ! nil { panic(err) } // ... }模拟 Azure ACR 凭据助手docker-credential-acr-envimport ( github.com/chrismellard/docker-credential-acr-env/pkg/credhelper github.com/google/go-containerregistry/pkg/authn github.com/google/go-containerregistry/pkg/v1/remote ) func main() { // ... acrHelper : credhelper.NewACRCredentialsHelper() img, err : remote.Get(ref, remote.WithAuthFromKeychain(authn.NewKeychainFromHelper(acrHelper))) if err ! nil { panic(err) } // ... }同理原 README 还提到pkg/v1/google.Keychain实现了对docker-credential-gcr的模拟通过google.NewEnvAuthenticator与google.NewGcloudAuthenticator从环境/gcloud中取 GCR 凭据。需要说明的是该google子包属于上游 go-containerregistry 的组成部分未包含在本仓库的 vendored 目录中vendor/github.com/google/go-containerregistry/pkg/v1/下仅有remote等子包使用前需自行引入上游依赖。5.2NewMultiKeychain按优先级组合多个凭据来源当集群同时涉及 Docker Hub、GCR、ECR、ACR 时单一 Keychain 往往不够。NewMultiKeychainmultikeychain.go#L27-L46将多个 Keychain 组合为一个解析时按给定顺序逐个尝试遇到第一个非Anonymous的认证结果即返回全部失败则回落Anonymouskc : authn.NewMultiKeychain( authn.DefaultKeychain, google.Keychain, authn.NewKeychainFromHelper(ecr.ECRHelper{ClientFactory: api.DefaultClientFactory{}}), authn.NewKeychainFromHelper(acr.ACRCredHelper{}), )上述组合的解析语义来自原 README 及源码确认首先检查 Docker config 文件中的凭据其次检查环境中的 GCP 凭据再次通过模拟 ECR 凭据助手获取凭据最后通过模拟 ACR 凭据助手获取凭据若任一 Keychain 命中则直接采用不再咨询后续 Keychain全部未命中则使用Anonymous。其实现multikeychain.go#L36-L46本质是顺序循环 auth ! Anonymous短路判断因此组合顺序即优先级顺序这一点对多云场景的配置顺序有直接指导意义。5.3 补充RefreshingKeychainRefreshingKeychain 是对内层 Keychain 的缓存包装首次解析后把Authenticator缓存一段时间duration过期后重新解析。源码中通过refreshing结构体的expired()判断now().Sub(last) duration适合 ECR 等短期 token 的自动刷新场景避免每次请求都重新调用云厂商 API。六、Docker Config Authconfig.json 的完整解读以下内容整理自原 README 的 Docker Config Auth 章节并补充了包内解码实现的源码依据。6.1 Plaintextauths字段的真相执行docker login后凭据会写入 config 文件Linux/macOS 为~/.docker/config.jsonWindows 为%USERPROFILE%\.docker\config.json或DOCKER_CONFIG指定位置。典型内容{ auths: { registry.example.com: { auth: QXp1cmVEaWFtb25kOmh1bnRlcjI } } }auths是一个以 registry 为键的映射auth字段是HTTP Basic Auth 格式的username:password做 base64 编码的结果$ echo QXp1cmVEaWFtb25kOmh1bnRlcjI | base64 -d AzureDiamond:hunter2重要警告这意味着你的凭据在配置文件中是明文存储的仅做了可逆的 base64 编码并非加密。这份配置与下面的写法完全等价{ auths: { registry.example.com: { username: AzureDiamond, password: hunter2 } } }这一等价关系在 CI/CD 场景中非常实用当 CI 通过环境变量提供用户名/密码时你可以手工构造该文件而无需调用docker login。从源码看AuthConfig的 JSON 编解码逻辑authn.go#L66-L128会自动在这两种表示间互转反序列化时若存在auth字段则用decodeDockerConfigFieldAuth解出用户名/密码authn.go#L102-L128的实现还兼容无 padding 的 base64——先判断字符串是否以结尾分别走base64.StdEncoding与base64.RawStdEncoding并用SplitN(decoded, :, 2)切分序列化时若用户名/密码非空则重新编码出auth字段。6.2 Helpers凭据助手的两种配置方式docker login会警告使用明文凭据不安全并建议改用凭据助手。配置方式有两种全局凭据助手credsStore{ credsStore: osxkeychain }按 registry 指定凭据助手credHelpers{ credHelpers: { gcr.io: gcr } }authn包通过github.com/docker/cli/cli/config.Load解析文件并调用必要的凭据助手复用了 Docker CLI 中ConfigFile registry 域名 →AuthConfig的完整逻辑。6.3 Credential Helper 协议详解凭据助手协议docker-credential-helpers允许用独立二进制提供凭据避免把凭据硬编码进配置文件。协议有多个动词其中get最为关键。给定如下配置{ credHelpers: { gcr.io: gcr, eu.gcr.io: gcr } }要获取gcr.io的凭据时在credHelpers映射中查到其助手名为gcr拼接成docker-credential-前缀得到二进制名docker-credential-gcr必须位于$PATH随后通过STDIN 传入 registry 域名调用它$ echo gcr.io | docker-credential-gcr get {Username:_token,Secret:long access token}因为同一助手可以为多个 registry 服务所以必须把域名通过 STDIN 传入。访问eu.gcr.io时$ echo eu.gcr.io | docker-credential-gcr get {Username:_token,Secret:long access token}6.4 调试凭据助手tee 与 hardcoded 两个实用技巧凭据助手配置了但不好使是常见痛点。原 README 给出了两个调试利器技巧 1硬编码假助手——把假凭据写死验证是否走到了助手这一步#!/usr/bin/env bash echo {Username:token,Secret:hunter2}技巧 2tee 嗅探助手——转发真实助手的输出到 stderr观察真正发送给仓库的凭据#!/usr/bin/env bash docker-credential-gcr $ | tee (cat 12)将这两个脚本放到$PATH中分别命名为docker-credential-hardcoded和docker-credential-tee然后修改配置{ credHelpers: { gcr.io: tee, eu.gcr.io: hardcoded } }docker-credential-tee技巧对crane和docker都有效——原 README 展示了同一配置下crane manifest gcr.io/google-containers/pause与docker pull gcr.io/google-containers/pause都会在 stderr 打印出被截断的 token从而确认认证链路的每一步是否按预期执行。七、仓库侧认证Token 与 OAuth2 两条路径注册表对客户端认证提供两种方式token与oauth2两者最终都用于获取一个不透明的Bearer 令牌RegistryToken放入Authorization请求头。流程起点是registry 在版本检查API version check或常规操作期间返回401 Unauthorized并在响应头Www-Authenticate中携带质询challenge指示客户端如何继续。7.1 Token 方式Basic Auth 交换当AuthConfig中包含Username/Password或Auth字段时走 token 方式客户端用 Basic 凭据向 registry 的 token 服务换取 Bearer token再携带该 token 访问镜像。7.2 OAuth2 方式IdentityToken 交换当AuthConfig中包含IdentityToken字段时走 oauth2 方式。一个典型的触发场景是凭据助手返回Username:token——注意token不是占位符而是字面字符串这是 Docker 凭据助手协议中的特殊约定原 README 也注明其原因并不明确可参考 moby 相关 issue 讨论。需要特别说明的实现限制源自原 READMEauthn只支持 oauth2 的refresh_tokengrant_type。原因也很直白——仅凭 registry 的响应无法可靠判断应使用 oauth 还是 token 方式而 token 方式已被绝大多数 registry 广泛实现因此选择保守处理。八、给 kOps 使用者的实战要点小结默认匿名pkg/v1/remote的默认认证器是Anonymous访问私有仓库必须显式传入remote.WithAuthFromKeychain(...)或remote.WithAuth(...)且二者不可混用优先复用DefaultKeychain只要本机docker login可用包括$DOCKER_CONFIG、$REGISTRY_AUTH_FILE、Podman 的$XDG_RUNTIME_DIR/containers/auth.jsonkOps 相关代码如 copyimage.go即可直接复用同一份凭据多云场景用NewMultiKeychain排优先级顺序即优先级命中即短路全部未命中回落匿名短期令牌用RefreshingKeychain缓存降低对云厂商凭据服务的调用频率凭据不生效时的排查路径确认 config.json 路径与键名Docker Hub 是https://index.docker.io/v1/→ 确认credHelpers助手名与二进制存在性 → 用docker-credential-tee嗅探实际发送的凭据 → 检查返回的是Username/Password还是Username:token后者走 OAuth2/IdentityToken 路径。整个authn包的设计精髓在于把从哪拿凭据Keychain与凭据长什么样Authenticator彻底解耦并尽量复用 Docker 生态的既有配置与协议从而让 kOps 等依赖它的项目在面对异构仓库生态时只需组合而非重造认证逻辑。延伸阅读包内源码vendor/github.com/google/go-containerregistry/pkg/authn核心接口与实现均在 8 个 Go 文件中remote选项定义vendor/github.com/google/go-containerregistry/pkg/v1/remote/options.goWithAuth/WithAuthFromKeychain及互斥校验kOps 实际调用示例pkg/assets/assetcopy/copyimage.go镜像跨仓库拷贝时复用DefaultKeychainkOps 资产与镜像配置文档docs/operations/asset-repository.md、docs/operations/images.md【免费下载链接】kopsKubernetes Operations (kOps) - Production Grade k8s Installation, Upgrades and Management项目地址: https://gitcode.com/gh_mirrors/kop/kops创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表