安全开发-Harbor

前言

Rancher 集群、Gitea 代码与流水线、Nexus 制品源就位之后,还差一环:CI/CD 的最终产物——容器镜像放哪?内部镜像不宜公开,本文在内网自建私有镜像仓库 Harbor(CNCF 毕业项目,可理解为“自托管 Docker Hub“,多权限管理、robot 账号、内置 Trivy 扫描、pull-through 镜像代理)。

本文做三件事:搭建(Rancher chart + Gitea OIDC 登录)、使用(仓库管理、pull-through 缓存、项目权限、docker login/pull/push)、CI/CD 联动(robot 账号推镜像 + Trivy 扫描结果)。

1 背景概念

1.1 镜像名、tag 与 digest

镜像名完整写法分三段:

registry 域名 / 项目名 / 镜像名 : tag
mirror.kali-team.internal / dev-sec-ops / example-server : 1.0.0
└─ 从哪取(registry)┘  └── 分组 ──┘ └── 哪个镜像 ──┘ └─ 版本标签

tag 是版本标签(1.0.0、latest);digest(sha256:...)是内容指纹,内容相同则指纹相同。流水线给同一镜像推 1.0.0 和 latest 两个 tag、指向同一 digest,即“版本号与最新版指向同一份内容“。

1.2 project 与可见性

project 是镜像的分组,有可见性:公开(登录即可拉,配置允许时匿名也可)/ 私有(仅授权用户、组可拉)。本环境约定:构建产物一律进私有 project;library 是 Harbor 自动建的默认公开 project。

1.3 pull-through 缓存

内网到不了公网,Harbor 的 pull-through cache(镜像代理)解决拉取问题:

拉 mirror.kali-team.internal/docker.io/nats:latest 时,Harbor 本地没有就替你去上游取一份、缓存本地再给你,下次命中本地缓存。

上游源在 4.1 登记。设计约束:代理项目只读,不能往里 push。

1.4 最小权限

人走 OIDC + CLI 密码,组只给只读,robot 只给最小写权限。

2 搭建:Rancher chart 部署 Harbor

2.1 chart 版本

Harbor chart 版本 1.19.2,对应应用版本 2.15.2,部署在 mirror 命名空间。

2.2 添加 chart 仓库并创建应用

在 Rancher 的 Chart 仓库里添加 Harbor 官方 chart 仓库 https://helm.goharbor.io/ 并更新,然后从目录创建应用:

Rancher 里的 harbor chart 页面

图 1:Rancher Chart 目录中的 harbor chart(1.19.2,应用版本 2.15.2)

2.3 Helm values 改动

安装的时候基本不需要修改配置,我只改了 ingress 的域名。相对 chart 默认值的 diff 如下:

@@ ingress annotations @@
       ingress.kubernetes.io/ssl-redirect: 'true'
       nginx.ingress.kubernetes.io/proxy-body-size: '0'
       nginx.ingress.kubernetes.io/ssl-redirect: 'true'
-    className: ''
+      traefik.ingress.kubernetes.io/request-buffering: 'off'
+      traefik.ingress.kubernetes.io/response-buffering: 'off'
+    className: traefik
     controller: default
     hosts:
-      core: core.harbor.domain
+      core: mirror.kali-team.internal
     kubeVersionOverride: ''
     labels: {}
@@ ingress tls @@
     auto:
       commonName: ''
     certSource: auto
-    enabled: true
+    enabled: false
     secret:
       secretName: ''
   type: ingress
-externalURL: https://core.harbor.domain
+externalURL: https://mirror.kali-team.internal
   harborAdminPassword: <HARBOR_INITIAL_ADMIN_PASSWORD>
   imagePullPolicy: IfNotPresent
   imagePullSecrets: null

逐项说明:

配置原因
hosts.core: mirror.kali-team.internal核心改动:chart 默认占位域名 core.harbor.domain 内网不通,换成内网 DNS 域名
className / buffering 两行ingress 走集群现有的 Traefik 网关,顺手关掉缓冲(镜像层可能上百 MB,流式通过更稳)
tls.enabled: false证书由网关统一终结(内网 CA 签发),关 chart 自带 TLS 避免“两套证书“
externalURLHarbor 用它生成 OIDC 回调 / 跳转地址,必须等于浏览器实际打开的域名
harborAdminPassword初始 admin 密码,全文占位;会写进 K8s Secret,忘了可从 Secret 取回

其余 values 保持 chart 默认。

2.4 安装、取回初始密码并验证

在 chart 页面点部署,很快安装完成。输出里看这几行就够:

NAME: mirror                  # 应用(release)名:mirror
LAST DEPLOYED: Sun Sep 13 14:14:15 2026
NAMESPACE: mirror             # 部署到 mirror 命名空间
STATUS: deployed              # 部署完成
REVISION: 1
NOTES:
Please wait for several minutes for Harbor deployment to complete.
Then you should be able to visit the Harbor portal at https://mirror.kali-team.internal

初始密码若没记住,从 chart 建的 Secret 里取回:

# 做什么:读 mirror 命名空间里 mirror-harbor-core 这个 secret 的 HARBOR_ADMIN_PASSWORD 字段
# 怎么做:kubectl 按 jsonpath 取出该字段(base64 形式),管道给 base64 -d 解码
kubectl get secret mirror-harbor-core -n mirror -o jsonpath="{.data.HARBOR_ADMIN_PASSWORD}"|base64 -d
# 输出:<HARBOR_INITIAL_ADMIN_PASSWORD>(已脱敏)

用 admin + 初始密码登录,首次登录后建议改密码,新密码按凭据规范存放。

Harbor 项目页(admin 登录态)

图 2:Harbor 首次登录后的项目列表:默认公开 project library 已就位(同时验证 ingress 域名、内网 CA 证书、服务本体)

3 配置 OIDC 登录:用 Gitea 账号进 Harbor

Gitea 是 OIDC 身份提供方(IdP),接入方式与其它平台相同,两侧各一步。

3.1 Gitea 侧:创建 OAuth2 应用

dev-sec-ops 组织 → 设置 → 应用 → 创建 OAuth2 应用程序:

  • 应用名称:Harbor;
  • 重定向 URL:https://mirror.kali-team.internal/c/oidc/callback(Harbor 侧的固定回调地址,在 Harbor 配置管理最下方可见,照着抄即可);
  • 客户端类型:机密客户端(confidential)。

创建后 Gitea 颁发 Client ID / Client Secret,占位 <client-id> / <client-secret>,只写进 Harbor 配置。

Gitea 组织里的 Harbor OAuth2 应用

图 3:Gitea dev-sec-ops 组织的 OAuth2 应用:应用名 Harbor,重定向 URL 指向 Harbor 的 OIDC 回调,机密客户端(Client ID / Secret 已打码)

3.2 Harbor 侧:填 OIDC 设置

系统管理 → 配置管理,OIDC 一节的关键项:

字段值
启用 OIDC开
提供方名称Gitea(登录按钮上显示的名称)
Endpoint(Issuer)https://hub.kali-team.internal(Harbor 读它的 .well-known/openid-configuration 发现端点,自动找到授权 / 令牌端点)
Client ID / Client Secret<client-id> / <client-secret>
组声明(group claim)groups(Harbor 按它同步用户组成员关系)
Scopesopenid,profile,email,groups

Harbor 的 OIDC 配置

图 4:Harbor 配置管理的 OIDC 一节:提供方 Gitea,Endpoint 指向 hub.kali-team.internal,组声明 groups(Client ID 已打码)

3.3 验证

登出 admin 回到登录页,出现两个入口:“通过 gitea 登录“与”通过本地数据库登录“。点前者,浏览器跳 Gitea 授权同意页后自动回 Harbor,全程无密码登录,右上角显示当前 Gitea 身份。

Gitea 授权同意页

图 5:Gitea 授权同意页:授权 Harbor 访问您的帐户,应用由 @dev-sec-ops 创建

Harbor 登录页

图 6:启用 OIDC 后的 Harbor 登录页:「通过 gitea 登录」与「通过本地数据库登录」两个入口

4 使用:仓库、项目与权限

4.1 仓库管理:登记五个上游源

仓库管理(系统管理 → 仓库管理)是上游源登记处。本环境登记 5 个代理目标:registry.k8s.io、quay.io、ghcr.io、gcr.io、docker.io,上游指向 daocloud,状态全部健康:

Harbor 仓库管理 图 7:五个代理目标,上游指向 daocloud 镜像源,全部健康

注意:这些是部分镜像源,docker/dind、docker/buildx、moby/buildkit 会 404(详见“常见问题与坑“)。

4.2 创建 pull-through 项目:docker.io

建项目 docker.io,打开镜像代理开关,上游 URL 指向 https://docker.m.daocloud.io:

新建 pull-through 项目 docker.io

图 8:新建项目 docker.io:打开“镜像代理“开关,上游 URL 指向 daocloud 镜像源

实操:

# 做什么:登录私有仓库(docker CLI 只认"用户名 + 密码",不走 OIDC)
docker login mirror.kali-team.internal
# Username: Kali-Team(OIDC 用户)  Password: (略,4.4 设置的 CLI 密码)
# 输出 Login Succeeded;注意 docker 默认把凭据明文存进 ~/.docker/config.json,
# 个人机器上可接受,共享机器建议配置 credential helper(输出里有警告提示)

# 做什么:经由代理拉一个镜像
docker pull mirror.kali-team.internal/docker.io/nats:latest
# 首拉:Harbor 替我们从上游 daocloud 取回并缓存本地
# Digest: sha256:c50b0a773c6c8d033cad54f7c53cb97fd6e09b3bfc1accda7cd4fa94e19e9ed9

再拉一次命中本地缓存。上游没有该镜像时 Harbor 透传 404——先确认上游能否拉到,别误以为是 Harbor 配错。

4.3 权限:给 dev-sec-ops 组“访客“角色

代理项目内容来自公开上游,团队只需读权限。项目成员管理页添加 dev-sec-ops 组,角色访客(guest)(Harbor 项目级角色三种:管理员 / 开发者 / 访客,1.4 的最小权限体现):

项目添加组成员(访客角色)

图 9:docker.io 项目的成员管理中添加 dev-sec-ops 组,角色为访客(只读)

4.4 OIDC 用户的 CLI 密码

换普通用户(Kali-Team)登录,右上角 → 个人信息 → 用户设置:可见身份来源 “Onboarded via OIDC provider”(首次 OIDC 登录自动建号),以及一个 CLI 密码字段。OIDC 只服务 Web 登录,docker login 只认“用户名 + 密码“或 robot 账号,所以每个用户单独留了一个 CLI 密码,设置后 4.2 的登录才能跑通(详见“常见问题与坑“):

用户设置页

图 10:Kali-Team 账号的用户设置:CLI 密码供 docker login 使用(界面显示为圆点)

项目页可看到 docker.io 与 library 两个 project;“其他操作“菜单里有导出 CVEs,把漏洞扫描结果导出成文件(第 5 章):

项目页与导出 CVEs 菜单

图 11:项目列表:“其他操作“菜单里的“导出 CVEs”

4.5 开发机手动推送

四步:前提是机器信任内网 CA(docker login/push 走 https,加信任与流水线 “Verify Certificate” 是同一套双通道,6.1);然后 docker login(CLI 密码);项目页的推送命令按钮直接给出完整目标地址(形如 mirror.kali-team.internal/<项目名>/<仓库名>:<tag>,以页面显示为准);最后:

# 做什么:把本地镜像改名为 Harbor 目标地址,再推送
# 怎么做:docker tag 给本地镜像起"新名字"(不复制内容,只是加别名);
#         docker push 把镜像内容推到目标
# 为什么:tag 的目标地址必须与项目页显示一字不差(项目名、仓库名、tag)
docker tag <本地镜像>:<本地tag> mirror.kali-team.internal/<项目名>/<仓库名>:<tag>
docker push mirror.kali-team.internal/<项目名>/<仓库名>:<tag>

验证:项目页该仓库的 Artifacts 列表出现新 tag,或换一台机器 docker pull 同一地址。边界:pull-through 代理项目只读、push 被拒(详见“常见问题与坑“),构建产物进私有项目 dev-sec-ops。podman push 与 helm push oci://mirror.kali-team.internal/<项目名>(以 OCI 格式推 Helm Chart)是同一套“登录 + 目标地址“逻辑。

4.6 主界面速览

  1. 项目列表(登录后主界面):所有项目的入口;
  2. 仓库列表(项目页):项目下的仓库(即镜像),代理项目这里就是从上游取回并缓存的镜像列表;
  3. 镜像详情(Artifacts 页):一行一个 tag——tag 名、digest、大小、推送 / 拉取时间、可直接复制的 docker pull 命令,页面还有按需扫描入口。

5 镜像扫描:让 Trivy 盯着镜像

5.1 扫描器是现成的:Trivy

chart 自带 Trivy 扫描器,不用另装。配置页显示:Trivy v0.72.0,默认扫描器且已启用,支持漏洞扫描与 SBOM 生成(SBOM = 软件物料清单),严重级别 UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL 全开:

Trivy 扫描器配置

图 12:Harbor 镜像扫描器页:Trivy v0.72.0(默认、已启用),严重级别 UNKNOWN–CRITICAL 全开

Trivy 拉下镜像、解开系统包与应用依赖,逐个对照漏洞数据库匹配(漏洞库由 Trivy 后台下载,下载慢 / 不全时换镜像源,详见“常见问题与坑“)。触发方式两种:按需(镜像页点“扫描“)与自动(推送后按扫描策略)。

5.2 被扫的镜像从哪来

代理项目的仓库列表就是 pull-through 缓存的台账:goharbor/*(Harbor 自家组件)、bitnamilegacy/*(Nexus / Dependency-Track 用的共享 PostgreSQL 等)、nats、postgres、rancher 等共 99 条:

docker.io 项目的镜像仓库列表

图 13:docker.io 项目的镜像仓库列表:goharbor/、bitnamilegacy/、nats、postgres、rancher 等共 99 条

5.3 扫描结果怎么读

以 library/nats:2.12.6-alpine 为例(图 14)。每条漏洞看四样:CVE 编号、严重级别、受影响组件、是否有修复版本:

  • CVE-2026-31789:危急(critical),组件 libcrypto3 / libssl3(OpenSSL 运行时库),有修复版本;
  • CVE-2026-22184:严重(high),组件 zlib,有修复版本。

扫描结果示例

图 14:library/nats 2.12.6-alpine 的扫描结果:CVE-2026-31789(危急,libcrypto3/libssl3)、CVE-2026-22184(严重,zlib)等,均已显示修复版本

动作:有修复版本 → 把镜像升到含修复的版本;没有 → 评估可利用性(KEV:已知在野利用的漏洞优先级最高)并记录风险接受。要留审计痕迹就用项目页“导出 CVEs“(图 11)。

6 CI/CD 联动:docker.yaml 怎么用 Harbor

本节只讲 Harbor 为流水线提供的三样东西:账号、证书、动作。

6.1 robot 账号:CI 专用账号

流水线推镜像用 robot 账号 robot-dev-sec-ops+ci-runner:project 级(只作用于 dev-sec-ops)、repository pull+push、长期有效。密码创建时只显示一次,存进 Gitea 仓库 Secret 的 MIRROR_USER / MIRROR_TOKEN。

API 创建时有两个要点:kind 字段不能少;duration: 0 不合法,长期有效要写 -1。

Harbor robot 账号列表

图 15:dev-sec-ops 项目 → Robot Accounts:robot-dev-sec-ops+ci-runner(2 项权限、Never Expires)

6.2 CA 注入:信任内网证书的双通道

任务容器默认不信任内网 CA,流水线在 “Verify Certificate” 步骤注入两处:

sudo cp kali-team-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates                                # 通道一:系统信任库(git/curl 等 HTTPS 用它)
sudo mkdir -p /etc/docker/certs.d/mirror.kali-team.internal
sudo cp kali-team-ca.crt /etc/docker/certs.d/mirror.kali-team.internal/ca.crt   # 通道二:docker 客户端"按 registry"的 CA

6.3 登录 / 推送 / 验证:三个动作

      - name: Login to Harbor Registry 登录Harbor Registry
        uses: https://hub.kali-team.internal/actions/login-action@v2
        with:
          registry: https://mirror.kali-team.internal
          username: ${{ secrets.MIRROR_USER }}
          password: ${{ secrets.MIRROR_TOKEN }}
      # (中间为 checkout / QEMU / 缓存 / buildx 配置)
      - name: Build and push Docker image for ${{ matrix.service }}
        uses: https://hub.kali-team.internal/actions/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64
          push: true
          target: ${{ matrix.service }}
          tags: |
            mirror.kali-team.internal/dev-sec-ops/example-${{ matrix.service }}:${{ needs.setup.outputs.version }}
            mirror.kali-team.internal/dev-sec-ops/example-${{ matrix.service }}:latest
          # (build-args 与 cache-from/to 参数见 Build and push 步骤)
      - name: Inspect image
        run: |
          docker buildx imagetools inspect mirror.kali-team.internal/dev-sec-ops/example-${{ matrix.service }}:${{ needs.setup.outputs.version }}
  • login-action@v2 把 robot 凭据写进任务容器的 ~/.docker/config.json,后续 push 自动带上;
  • build-push-action@v6 构建并直接推入 Harbor,一次打两个 tag;产物落私有 project dev-sec-ops(代理项目只读,推不进去);
  • docker buildx imagetools inspect 不拉镜像,只向 registry 要 manifest——能打印出来即证明镜像已在 Harbor,完成“推送成功“自检。

顺带一提:QEMU 注册用的镜像也走代理(mirror.kali-team.internal/docker.io/tonistiigi/binfmt:latest)——pull-through 在流水线里的实际用法。

6.4 流水线推送验证:run #6

docker.yaml 手动触发(build_server=true、version=1.0.0):setup job 5s 算出版本号,build-and-push job 32s 走完全流程(总耗时 1m25s)。dev-sec-ops 项目出现 example-server——tags 1.0.0 与 latest 指向同一 digest sha256:4cd55…e3513(约 49.6 MiB)。

Harbor dev-sec-ops 项目页

图 16:dev-sec-ops 项目页(Repositories 视图):example-server / gitea/gomod-cache / gitea/runner-images 三个镜像仓库已入库

6.5 三类权限一览

身份类型作用范围权限谁在用
Kali-Team人(OIDC)所属项目Web + CLI(CLI 密码,4.4)人用 docker login/pull
dev-sec-ops 组组docker.io 项目访客(只读)团队拉公开镜像
robot-dev-sec-ops+ci-runnerrobotdev-sec-ops 项目repository pull+pushCI 流水线推镜像

7 常见问题与坑

  1. 上游镜像源不全:docker/dind、docker/buildx、moby/buildkit 全部 404(daocloud 是部分镜像源,经代理拉取 404 易误判为 Harbor 配置问题)。绕行方案:docker CLI 拷宿主二进制进任务镜像、buildx 用宿主 Go 源码编译烤入、buildx 选 docker 驱动——去掉对这三个公网镜像的依赖。
  2. OIDC 登录 ≠ 能 docker login:OIDC 只服务 Web,CLI 只认“用户名 + 密码“或 robot;OIDC 用户没设 CLI 密码时 docker login 认证失败。
  3. externalURL 必须随域名一起改:Harbor 用它生成回调 / 跳转地址,只改 hosts.core 而 externalURL 留默认 core.harbor.domain,OIDC 授权会跳去不存在的域名。
  4. Trivy 漏洞库没下载完成(库体积大、下载源不稳):扫描慢或失败时换镜像源;重要扫描建议搭建期先预热一遍。
  5. pull-through 项目推不进去(设计约束,非故障):代理项目内容由上游定义、只读;构建产物必须进普通私有项目(dev-sec-ops)——这也是 robot 只授该项目 pull+push 的原因。
  6. 初始 admin 密码的存放:chart 写进 secret mirror-harbor-core;首次登录后改密,按团队凭据规范存放。

8 小结

Harbor(2.15.2)装进 k3s,接上 Gitea 单点登录,配好五个平台镜像代理,docker.yaml 流水线用 robot 账号推镜像、imagetools inspect 自检,Trivy 持续扫镜像漏洞:

  • 搭建:diff 只动三组——ingress 域名、关 chart 自带 TLS、externalURL;
  • OIDC:Gitea 应用 + Harbor OIDC 设置,与其它平台共用统一登录;
  • 使用:仓库管理登记上游、代理项目取回缓存、组授只读、CLI 密码走 CLI;
  • 扫描:Trivy 内置,按需 / 自动触发,结果按“CVE / 级别 / 组件 / 修复版本“读;
  • CI/CD:robot + CA 双通道 + 登录 / 推送 / 验证三动作,无人持有 admin 密码;

下一步可选:镜像签名(cosign)、复制(replication)(跨集群 / 备份仓库同步)、把扫描结果接进流水线质量门禁(命中危急漏洞即卡发版)。

参考