安全开发-Harbor
目录
在内网 DevSecOps 体系里部署 Harbor(私有容器镜像仓库):Rancher chart 搭建、Gitea OIDC 单点登录、五个平台镜像代理(pull-through 缓存),再用 robot 账号把 Gitea Actions 流水线的镜像推上去,让 Trivy 扫出镜像漏洞 Rancher 集群、Gitea 代码与流水线、Nexus 制品源就位之后,还差一环:CI/CD 的最终产物——容器镜像放哪?内部镜像不宜公开,本文在内网自建私有镜像仓库 Harbor(CNCF 毕业项目,可理解为“自托管 Docker Hub“,多权限管理、robot 账号、内置 Trivy 扫描、pull-through 镜像代理)。 本文做三件事:搭建(Rancher chart + Gitea OIDC 登录)、使用(仓库管理、pull-through 缓存、项目权限、 镜像名完整写法分三段: tag 是版本标签( project 是镜像的分组,有可见性:公开(登录即可拉,配置允许时匿名也可)/ 私有(仅授权用户、组可拉)。本环境约定:构建产物一律进私有 project; 内网到不了公网,Harbor 的 pull-through cache(镜像代理)解决拉取问题: 拉 上游源在 4.1 登记。设计约束:代理项目只读,不能往里 push。 人走 OIDC + CLI 密码,组只给只读,robot 只给最小写权限。 Harbor chart 版本 1.19.2,对应应用版本 2.15.2,部署在 在 Rancher 的 Chart 仓库里添加 Harbor 官方 chart 仓库 图 1:Rancher Chart 目录中的 harbor chart(1.19.2,应用版本 2.15.2) 安装的时候基本不需要修改配置,我只改了 ingress 的域名。相对 chart 默认值的 diff 如下: 逐项说明: 其余 values 保持 chart 默认。 在 chart 页面点部署,很快安装完成。输出里看这几行就够: 初始密码若没记住,从 chart 建的 Secret 里取回: 用 图 2:Harbor 首次登录后的项目列表:默认公开 project library 已就位(同时验证 ingress 域名、内网 CA 证书、服务本体) Gitea 是 OIDC 身份提供方(IdP),接入方式与其它平台相同,两侧各一步。 创建后 Gitea 颁发 Client ID / Client Secret,占位 图 3:Gitea dev-sec-ops 组织的 OAuth2 应用:应用名 Harbor,重定向 URL 指向 Harbor 的 OIDC 回调,机密客户端(Client ID / Secret 已打码) 系统管理 → 配置管理,OIDC 一节的关键项: 图 4:Harbor 配置管理的 OIDC 一节:提供方 Gitea,Endpoint 指向 hub.kali-team.internal,组声明 groups(Client ID 已打码) 登出 admin 回到登录页,出现两个入口:“通过 gitea 登录“与”通过本地数据库登录“。点前者,浏览器跳 Gitea 授权同意页后自动回 Harbor,全程无密码登录,右上角显示当前 Gitea 身份。 图 5:Gitea 授权同意页:授权 Harbor 访问您的帐户,应用由 @dev-sec-ops 创建 图 6:启用 OIDC 后的 Harbor 登录页:「通过 gitea 登录」与「通过本地数据库登录」两个入口 仓库管理(系统管理 → 仓库管理)是上游源登记处。本环境登记 5 个代理目标: 注意:这些是部分镜像源, 建项目 图 8:新建项目 docker.io:打开“镜像代理“开关,上游 URL 指向 daocloud 镜像源 实操: 再拉一次命中本地缓存。上游没有该镜像时 Harbor 透传 404——先确认上游能否拉到,别误以为是 Harbor 配错。 代理项目内容来自公开上游,团队只需读权限。项目成员管理页添加 图 9:docker.io 项目的成员管理中添加 dev-sec-ops 组,角色为访客(只读) 换普通用户(Kali-Team)登录,右上角 → 个人信息 → 用户设置:可见身份来源 “Onboarded via OIDC provider”(首次 OIDC 登录自动建号),以及一个 CLI 密码字段。OIDC 只服务 Web 登录, 图 10:Kali-Team 账号的用户设置:CLI 密码供 项目页可看到 图 11:项目列表:“其他操作“菜单里的“导出 CVEs” 四步:前提是机器信任内网 CA( 验证:项目页该仓库的 Artifacts 列表出现新 tag,或换一台机器 chart 自带 Trivy 扫描器,不用另装。配置页显示:Trivy v0.72.0,默认扫描器且已启用,支持漏洞扫描与 SBOM 生成(SBOM = 软件物料清单),严重级别 图 12:Harbor 镜像扫描器页:Trivy v0.72.0(默认、已启用),严重级别 UNKNOWN–CRITICAL 全开 Trivy 拉下镜像、解开系统包与应用依赖,逐个对照漏洞数据库匹配(漏洞库由 Trivy 后台下载,下载慢 / 不全时换镜像源,详见“常见问题与坑“)。触发方式两种:按需(镜像页点“扫描“)与自动(推送后按扫描策略)。 代理项目的仓库列表就是 pull-through 缓存的台账: 图 13:docker.io 项目的镜像仓库列表:goharbor/、bitnamilegacy/、nats、postgres、rancher 等共 99 条 以 图 14:library/nats 2.12.6-alpine 的扫描结果:CVE-2026-31789(危急,libcrypto3/libssl3)、CVE-2026-22184(严重,zlib)等,均已显示修复版本 动作:有修复版本 → 把镜像升到含修复的版本;没有 → 评估可利用性(KEV:已知在野利用的漏洞优先级最高)并记录风险接受。要留审计痕迹就用项目页“导出 CVEs“(图 11)。 本节只讲 Harbor 为流水线提供的三样东西:账号、证书、动作。 流水线推镜像用 robot 账号 API 创建时有两个要点: 图 15: 任务容器默认不信任内网 CA,流水线在 “Verify Certificate” 步骤注入两处: 顺带一提:QEMU 注册用的镜像也走代理( 图 16: Harbor(2.15.2)装进 k3s,接上 Gitea 单点登录,配好五个平台镜像代理, 下一步可选:镜像签名(cosign)、复制(replication)(跨集群 / 备份仓库同步)、把扫描结果接进流水线质量门禁(命中危急漏洞即卡发版)。 前言
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)┘ └── 分组 ──┘ └── 哪个镜像 ──┘ └─ 版本标签1.0.0、latest);digest(sha256:...)是内容指纹,内容相同则指纹相同。流水线给同一镜像推 1.0.0 和 latest 两个 tag、指向同一 digest,即“版本号与最新版指向同一份内容“。 1.2 project 与可见性
library 是 Harbor 自动建的默认公开 project。 1.3 pull-through 缓存
mirror.kali-team.internal/docker.io/nats:latest 时,Harbor 本地没有就替你去上游取一份、缓存本地再给你,下次命中本地缓存。 1.4 最小权限
2 搭建:Rancher chart 部署 Harbor
2.1 chart 版本
mirror 命名空间。 2.2 添加 chart 仓库并创建应用
https://helm.goharbor.io/ 并更新,然后从目录创建应用:
2.3 Helm values 改动
@@ 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 取回 2.4 安装、取回初始密码并验证
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# 做什么:读 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 + 初始密码登录,首次登录后建议改密码,新密码按凭据规范存放。
3 配置 OIDC 登录:用 Gitea 账号进 Harbor
3.1 Gitea 侧:创建 OAuth2 应用
dev-sec-ops 组织 → 设置 → 应用 → 创建 OAuth2 应用程序:Harbor;https://mirror.kali-team.internal/c/oidc/callback(Harbor 侧的固定回调地址,在 Harbor 配置管理最下方可见,照着抄即可);<client-id> / <client-secret>,只写进 Harbor 配置。
3.2 Harbor 侧:填 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 按它同步用户组成员关系)Scopes openid,profile,email,groups
3.3 验证


4 使用:仓库、项目与权限
4.1 仓库管理:登记五个上游源
registry.k8s.io、quay.io、ghcr.io、gcr.io、docker.io,上游指向 daocloud,状态全部健康:
图 7:五个代理目标,上游指向 daocloud 镜像源,全部健康docker/dind、docker/buildx、moby/buildkit 会 404(详见“常见问题与坑“)。 4.2 创建 pull-through 项目:docker.io
docker.io,打开镜像代理开关,上游 URL 指向 https://docker.m.daocloud.io:
# 做什么:登录私有仓库(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 4.3 权限:给 dev-sec-ops 组“访客“角色
dev-sec-ops 组,角色访客(guest)(Harbor 项目级角色三种:管理员 / 开发者 / 访客,1.4 的最小权限体现):
4.4 OIDC 用户的 CLI 密码
docker login 只认“用户名 + 密码“或 robot 账号,所以每个用户单独留了一个 CLI 密码,设置后 4.2 的登录才能跑通(详见“常见问题与坑“):
docker login 使用(界面显示为圆点)docker.io 与 library 两个 project;“其他操作“菜单里有导出 CVEs,把漏洞扫描结果导出成文件(第 5 章):
4.5 开发机手动推送
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>docker pull 同一地址。边界:pull-through 代理项目只读、push 被拒(详见“常见问题与坑“),构建产物进私有项目 dev-sec-ops。podman push 与 helm push oci://mirror.kali-team.internal/<项目名>(以 OCI 格式推 Helm Chart)是同一套“登录 + 目标地址“逻辑。 4.6 主界面速览
docker pull 命令,页面还有按需扫描入口。 5 镜像扫描:让 Trivy 盯着镜像
5.1 扫描器是现成的:Trivy
UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL 全开:
5.2 被扫的镜像从哪来
goharbor/*(Harbor 自家组件)、bitnamilegacy/*(Nexus / Dependency-Track 用的共享 PostgreSQL 等)、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,有修复版本。
6 CI/CD 联动:docker.yaml 怎么用 Harbor
6.1 robot 账号:CI 专用账号
robot-dev-sec-ops+ci-runner:project 级(只作用于 dev-sec-ops)、repository pull+push、长期有效。密码创建时只显示一次,存进 Gitea 仓库 Secret 的 MIRROR_USER / MIRROR_TOKEN。kind 字段不能少;duration: 0 不合法,长期有效要写 -1。
dev-sec-ops 项目 → Robot Accounts:robot-dev-sec-ops+ci-runner(2 项权限、Never Expires) 6.2 CA 注入:信任内网证书的双通道
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,完成“推送成功“自检。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)。
dev-sec-ops 项目页(Repositories 视图):example-server / gitea/gomod-cache / gitea/runner-images 三个镜像仓库已入库 6.5 三类权限一览
身份 类型 作用范围 权限 谁在用 Kali-Team 人(OIDC) 所属项目 Web + CLI(CLI 密码,4.4) 人用 docker login/pulldev-sec-ops 组 组 docker.io 项目访客(只读) 团队拉公开镜像 robot-dev-sec-ops+ci-runner robot dev-sec-ops 项目repository pull+push CI 流水线推镜像 7 常见问题与坑
docker/dind、docker/buildx、moby/buildkit 全部 404(daocloud 是部分镜像源,经代理拉取 404 易误判为 Harbor 配置问题)。绕行方案:docker CLI 拷宿主二进制进任务镜像、buildx 用宿主 Go 源码编译烤入、buildx 选 docker 驱动——去掉对这三个公网镜像的依赖。docker login:OIDC 只服务 Web,CLI 只认“用户名 + 密码“或 robot;OIDC 用户没设 CLI 密码时 docker login 认证失败。externalURL 必须随域名一起改:Harbor 用它生成回调 / 跳转地址,只改 hosts.core 而 externalURL 留默认 core.harbor.domain,OIDC 授权会跳去不存在的域名。dev-sec-ops)——这也是 robot 只授该项目 pull+push 的原因。mirror-harbor-core;首次登录后改密,按团队凭据规范存放。 8 小结
docker.yaml 流水线用 robot 账号推镜像、imagetools inspect 自检,Trivy 持续扫镜像漏洞:externalURL; 参考