概述

凌晨两点,手机震了一下。磁盘告警——95%。

爬起来一看,Harbor 服务器 /data/registry 目录占了 200GB,里面堆了 3000 多个 tag。有去年测试时推的 app:v0.0.3-beta,有构建失败残留的 app:feature-branch-xxx,还有十个版本的 base image 副本——每个都 800MB 起步。

这不是个例。我在多个团队见过同样的剧本:CI/CD 跑起来之后,镜像只管推不管清,三个月后磁盘就炸。更麻烦的是,有些镜像带着已知 CVE 漏洞跑在生产上,权限管理形同虚设——谁都能推、谁都能拉,项目之间没有隔离。

这篇文章解决三个问题:镜像怎么管权限、怎么拦漏洞、怎么清垃圾。我会用 Harbor(CNCF 毕业项目,目前 GitHub 29k+ star)从安装到生产配置走一遍,每一步都给出可直接复制的配置。如果你刚开始搭制品仓库,或者已经有了但管得稀里糊涂,这篇能帮你少走弯路。

为什么需要制品仓库

先说清楚一个概念:制品仓库(Artifact Repository)不是 Docker Hub

Docker Hub 是公共镜像托管平台,类似 npm 的 registry。你在上面拉官方镜像没问题,但拿它存公司业务镜像?有几个硬伤:

  1. 没有细粒度权限。Docker Hub 的 organization 只有 admin 和普通成员,做不到"测试团队能推 dev 项目、不能碰 prod 项目"
  2. 没有漏洞扫描。推上去就推上去了,CVE 该有还是有
  3. 没有保留策略。镜像只增不减,除非手动删
  4. 网络不稳定。国内拉 Docker Hub 限速、超时是家常便饭

Harbor 解决的就是这些问题。说白了,Harbor = Docker Registry + 权限管理 + 漏洞扫描 + 复制同步 + 审计日志。它像一个带安保、带分拣、带回收站的仓库,而不是一个露天堆场。

类比一下:Docker Hub 像公共停车场,谁都能进;Harbor 像企业内部车库,分区域(项目)、刷卡进(RBAC)、进门安检(漏洞扫描)、定期清退僵尸车(GC + 保留策略)。

Harbor 架构速览

不堆术语,直接看图:

                    ┌─────────────┐
                    │   客户端     │  docker push / pull
                    └──────┬──────┘
                    ┌──────▼──────┐
                    │   Nginx     │  反向代理 + TLS
                    │  (Portal)   │
                    └──────┬──────┘
          ┌────────────────┼────────────────┐
          │                │                │
   ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
   │  Core API   │ │  Registry   │ │  JobService  │
   │ (API+UI)    │ │ (Distribution)│ │ (扫描/复制/GC)│
   └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
          │               │                │
   ┌──────▼──────┐ ┌─────▼───────┐ ┌─────▼───────┐
   │ PostgreSQL  │ │   Storage   │ │   Trivy     │
   │ (元数据)     │ │ (文件系统)   │ │  (扫描引擎)  │
   └─────────────┘ └────────────┘ └─────────────┘

几个核心组件,一句话说清各自干嘛:

组件作用類比
Core处理 API 请求、管理项目和用户仓库前台
Registry实际存镜像文件,基于 Docker Distribution货架
JobService跑异步任务:扫描、复制、GC搬运工
PostgreSQL存元数据:项目、用户、镜像 tag 信息台账
Trivy扫描镜像里的 CVE 漏洞安检员
Redis缓存会话和作业状态便签纸

不需要记住每个组件的内部实现。你只要知道:推镜像走 Nginx → Core → Registry,扫描和 GC 走 JobService 异步执行。理解这个链路,后面排障就有方向了。

从零安装 Harbor

生产环境推荐用 Harbor 官方的 Helm Chart 部署到 K8s。但如果你刚开始验证,docker-compose 方式更快。下面两种方式都给出。

方式一:docker-compose(开发/测试环境)

# 下载 Harbor 离线安装包
wget https://github.com/goharbor/harbor/releases/download/v2.12.0/harbor-offline-installer-v2.12.0.tgz
tar xzf harbor-offline-installer-v2.12.0.tgz
cd harbor

# 复制配置模板
cp harbor.yml.tmpl harbor.yml

关键配置项(harbor.yml):

# HTTPS 配置——生产环境必须开 TLS
hostname: harbor.yourcompany.com
http:
  port: 80
https:
  port: 443
  certificate: /data/cert/harbor.crt
  private_key: /data/cert/harbor.key

# 管理员初始密码,首次登录后立即改
harbor_admin_password: Harbor12345  # 别用这个!

# 数据存储目录
data_volume: /data/harbor

# 数据库配置
database:
  password: change_this_to_random_string
  max_idle_conns: 50
  max_open_conns: 100

# Trivy 扫描器
trivy:
  ignore_unfixed: false  # 不要忽略未修复的漏洞
  skip_update: false    # 每次启动更新漏洞库
  offline_scan: false

# 垃圾回收
garbage_collection:
  enabled: true
  schedule: "0 3 * * *"  # 每天凌晨 3 点执行

# 审计日志
audit_log:
  enabled: true

安装和启动:

# 生成自签名证书(测试环境用,生产请用正式证书)
mkdir -p /data/cert
openssl req -x509 -newkey rsa:4096 -nodes \
  -keyout /data/cert/harbor.key \
  -out /data/cert/harbor.crt \
  -days 365 \
  -subj "/CN=harbor.yourcompany.com"

# 安装
./install.sh --with-trivy --with-chartmuseum

# 启动后验证
docker-compose ps  # 所有组件应该是 healthy

方式二:Helm Chart(生产环境)

# 添加 Harbor Helm 仓库
helm repo add harbor https://helm.goharbor.io
helm repo update

# 创建 values.yaml 覆盖默认配置
cat > harbor-values.yaml << 'EOF'
expose:
  type: ingress
  tls:
    enabled: true
    certSource: secret
    secret:
      secretName: harbor-tls

externalURL: https://harbor.yourcompany.com

# 持久化存储——生产环境必须配
persistence:
  persistentVolumeClaim:
    registry:
      size: 500Gi
      storageClass: managed-nfs-storage
    database:
      size: 20Gi
      storageClass: managed-nfs-storage
    trivy:
      size: 10Gi
      storageClass: managed-nfs-storage

# Trivy 扫描配置
trivy:
  enabled: true
  skipUpdate: false

# 垃圾回收
garbageCollection:
  enabled: true
  schedule: "0 3 * * *"

# 资源限制
resources:
  core:
    requests:
      cpu: 500m
      memory: 512Mi
    limits:
      cpu: 1000m
      memory: 1Gi

# 高可用(需要外置 PostgreSQL 和 Redis)
# 生产环境建议至少 2 个 Core 副本
core:
  replicas: 2
EOF

# 安装
helm install harbor harbor/harbor \
  -f harbor-values.yaml \
  -n harbor \
  --create-namespace

安装完成后,浏览器打开 https://harbor.yourcompany.com,用 admin 登录,第一件事改密码

项目与权限分层

Harbor 的权限模型很直观:用户 → 项目 → 角色。项目是隔离的基本单位,类似 GitLab 的 Group 或 GitHub 的 Organization。

为什么要用项目隔离

我在某出行项目踩过坑。一开始图省事,所有镜像都推到一个叫 library 的默认项目里。结果:

  • 测试同事手滑把 payment-service:v2 tag 覆盖了生产镜像
  • 没人分得清哪些是测试镜像、哪些是生产镜像
  • 等保审计时被指出"缺乏访问控制",直接扣分

后来拆成 devstagingprod 三个项目,权限分层后才理顺。这个教训记住:镜像仓库的第一道防线不是漏洞扫描,是项目隔离

四种角色和权限

Harbor 的项目角色就四种,别想复杂了:

角色能干什么类比
项目管理员管项目设置、加人、删镜像仓库主管
开发者推镜像、拉镜像、删自己的 tag搬运工
访客只能拉镜像取货人
维护人员扫描镜像、修改元数据质检员

生产环境推荐这样分配:

prod 项目:
  - SRE 团队 → 项目管理员
  - CI/CD 服务账号 → 开发者(只能 push,不能删)
  - 部署系统 → 访客(只能 pull)

dev 项目:
  - 开发团队 → 开发者
  - QA 团队 → 维护人员

用 API 批量管理权限

手动在 UI 上点几十个用户太累。Harbor 有完整的 REST API:

# 创建项目
curl -X POST "https://harbor.yourcompany.com/api/v2.0/projects" \
  -u "admin:your_password" \
  -H "Content-Type: application/json" \
  -d '{
    "project_name": "prod",
    "metadata": {
      "public": "false",
      "auto_scan": "true",
      "reuse_sys_cve_allowlist": "false"
    }
  }'

# 给项目添加成员
curl -X POST "https://harbor.yourcompany.com/api/v2.0/projects/1/members" \
  -u "admin:your_password" \
  -H "Content-Type: application/json" \
  -d '{
    "role_id": 2,  # 2=开发者, 3=访客
    "entity_type": "user",
    "entity_id": 5
  }'

CI/CD 流水线里推荐用机器人账号(Robot Account),而不是真实用户:

# 创建机器人账号(只有 push 权限)
curl -X POST "https://harbor.yourcompany.com/api/v2.0/projects/1/robots" \
  -u "admin:your_password" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ci-pusher",
    "level": "project",
    "duration": -1,
    "permissions": [{
      "access": [
        {"action": "pull", "resource": "repository"},
        {"action": "push", "resource": "repository"}
      ],
      "kind": "project",
      "namespace": "prod"
    }]
  }'

机器人账号的好处是:权限最小化、过期可控、不关联真人。流水线里用的就应该是这种账号,而不是某个人的 LDAP 账号——人离职了流水线就断了。

漏洞扫描:从检测到阻断

装了 Harbor 不开扫描,等于买了安检门不插电。

Trivy 集成原理

Harbor 从 v2.x 开始内置 Trivy 作为默认扫描引擎。Trivy 扫描的是镜像每一层(layer)里的软件包:apt 装的、pip 装的、npm 装的,全部查 CVE 数据库。

扫描分两种触发方式:

  1. 推送后自动扫描:镜像 push 完成后,JobService 拉起 Trivy 扫描
  2. 定时全量扫描:每天扫一遍所有已有镜像,因为漏洞库在更新,昨天的安全镜像今天可能就不安全了

配置自动扫描和阻断策略

在 Harbor UI 中:项目设置 → 勾选 Automatically scan images on push

更关键的是 阻止有漏洞的镜像被部署。Harbor 叫 “Deployment Security” 策略:

# 通过 API 设置项目级安全策略
# prevent_vul 等于 "true" 时,有高危漏洞的镜像不能被 pull
curl -X PUT "https://harbor.yourcompany.com/api/v2.0/projects/1" \
  -u "admin:your_password" \
  -H "Content-Type: application/json" \
  -d '{
    "metadata": {
      "auto_scan": "true",
      "prevent_vul": "true",
      "severity": "high"
    }
  }'

这段配置的含义:镜像存在 High 或 Critical 级别漏洞时,禁止 pull。severity 级别可选 nonelowmediumhighcritical,我推荐生产项目设为 high

这里有个踩坑点。prevent_vul 只对通过 Harbor 代理的 pull 请求生效。如果你的 K8s 节点直接配了 Docker daemon 的 insecure-registries 绕过 Harbor Core,阻断就不生效。解决办法:所有节点必须通过 Harbor 域名拉镜像,不走 IP 直连。

漏洞白名单

有些漏洞你暂时修不了——比如 base image 里的 glibc 版本,升了会炸。Harbor 支持配置 CVE 白名单:

# 全局 CVE 白名单
curl -X PUT "https://harbor.yourcompany.com/api/v2.0/system/CVEWhitelist" \
  -u "admin:your_password" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {"cve_id": "CVE-2024-12345"},
      {"cve_id": "CVE-2024-67890"}
    ],
    "expires_at": 1767225600
  }'

白名单必须有过期时间。不设过期时间的白名单等于永久放行,和没扫描一样。建议设 30 天,到期了要么修了漏洞要么续期——逼自己面对。

某次等保 2.0 审计,审计员专门查了白名单记录,发现有个 CVE 白名单开了 8 个月没动过,直接开了不符合项。教训:白名单是用来买时间的,不是用来逃避的。相关内容可以看相关文章:CI/CD 安全门禁的四层防御与误报治理,那篇专门讲了误报治理。

存储治理:保留策略与垃圾回收

这部分是开篇 200GB 磁盘爆炸的直接解法。

保留策略(Retention Policy)

保留策略解决一个问题:哪些 tag 该留、哪些该删。它按规则自动清理不需要的镜像 tag,不需要你手动删。

Harbor 支持的保留规则:

规则类型含义适用场景
保留最近 N 个 tag留最新的 N 个版本dev 项目,只需要能回滚的最近版本
保留 N 天内的 tag留近期推送的镜像feature 分支镜像,过期就删
保留匹配正则的 tagv* 格式的正式版prod 项目只保留发版镜像
保留最新 N 个 tag + 匹配正则组合规则prod 项目:留最近 5 个 v1.x 版本

推荐配置:

dev 项目:
  保留策略 → 保留最近 10 个 tag,其余删除
  执行频率 → 每天一次

staging 项目:
  保留策略 → 保留 7 天内的 tag
  执行频率 → 每天一次

prod 项目:
  保留策略 → 保留匹配 v* 的最近 20 个 tag
  执行频率 → 每周一次

通过 API 配置:

# 给 dev 项目设置保留策略:保留最近 10 个 tag
curl -X POST "https://harbor.yourcompany.com/api/v2.0/retentions" \
  -u "admin:your_password" \
  -H "Content-Type: application/json" \
  -d '{
    "scope": {
      "level": "project",
      "ref": 1
    },
    "trigger": {
      "kind": "Schedule",
      "settings": {
        "cron": "0 3 * * *"
      }
    },
    "rules": [{
      "action": "retain",
      "template": "latestPulledNCount",
      "params": {
        "latestPulledNCount": 10
      },
      "tag_selectors": [{
        "kind": "doublestar",
        "decoration": "matches",
        "pattern": "**"
      }]
    }]
  }'

注意:保留策略删除的是 tag 引用,不删底层 blob 文件。真正的磁盘空间回收要靠垃圾回收。

垃圾回收(Garbage Collection)

保留策略删了 tag,但镜像的层(blob)还在磁盘上。就像你删了快捷方式但安装包还在。垃圾回收(GC)就是清理这些无主 blob 的过程

Harbor 的 GC 基于 Docker Distribution 的 GC 机制:

# 手动触发 GC(建议先停 push 操作)
# Harbor UI → 系统 → 垃圾回收 → 立即执行

# 或通过 API
curl -X POST "https://harbor.yourcompany.com/api/v2.0/system/gc/schedule" \
  -u "admin:your_password" \
  -H "Content-Type: application/json" \
  -d '{
    "schedule": {
      "type": "Daily",
      "cron": "0 3 * * *"
    }
  }'

GC 执行时有个关键行为:会短暂阻塞 push 操作。因为 GC 需要将 Registry 切换到只读模式,扫描完所有 blob 标记无主的删除,再恢复读写。生产环境建议设在凌晨低峰期。

我推荐的开局操作(针对已经堆了很多镜像的老仓库):

# 1. 先配置保留策略,清理不需要的 tag
# 2. 等保留策略执行完,确认删除的 tag 确实不需要
# 3. 手动触发一次 GC
# 4. 查看 GC 日志确认释放了多少空间

# 查看 GC 执行结果
curl -s "https://harbor.yourcompany.com/api/v2.0/system/gc?page=1&page_size=5" \
  -u "admin:your_password" | python3 -m json.tool | grep -E "status|space_released"

实测数据:在一个 200GB、3000+ tag 的仓库上,配置"保留最近 10 个 tag"策略 + 执行一次 GC,磁盘占用从 200GB 降到 45GB。177GB 的 blob 是无主垃圾——没人引用但占着磁盘

存储配额

防止再次撑爆磁盘,给项目设配额:

# 给 dev 项目设 50GB 存储配额
curl -X PUT "https://harbor.yourcompany.com/api/v2.0/projects/1" \
  -u "admin:your_password" \
  -H "Content-Type: application/json" \
  -d '{
    "storage_limit": 53687091200  # 50GB in bytes
  }'

配额不是替代保留策略的,而是兜底。即使保留策略漏配了,配额也能在 50GB 时拒绝新 push,不至于把磁盘撑爆。

生产环境注意事项

高可用部署

docker-compose 方式是单点,不适合生产。生产环境用 Helm Chart 部署到 K8s 时:

  • Core 组件至少 2 副本,前面挂 Ingress
  • 数据库用外置 PostgreSQL,不要用 Harbor 自带的容器版(单点、不可扩)
  • Redis 用外置实例,避免重启丢会话
  • 存储用 NFS/Ceph/对象存储,不要用 emptyDir

高可用方案的前提是外置数据库和 Redis。Harbor 的无状态组件(Core、JobService、Portal)可以多副本,但有状态组件(PostgreSQL、Redis)如果用内置的,就是单点。这个设计选择直接决定你的 Harbor 能不能扛住节点故障。

复制策略做跨机房容灾

如果有多机房需求,Harbor 的复制(Replication)功能可以做镜像同步:

主 Harbor (机房A)  ──push──>  从 Harbor (机房B)
                              └── K8s 集群直接从机房B拉镜像

配置复制策略时注意三点:

  1. 过滤规则要精确。不要全量同步,按项目 + tag 前缀过滤,只同步 v* 的生产镜像
  2. 触发模式选手动或定时。实时同步(event-based)在镜像量大时会有延迟堆积
  3. 带宽限制。跨机房同步带宽别拉满,留出业务流量空间。Harbor 支持配置 --bandwidth 限制

在某次跨机房容灾方案设计中,我们用 Harbor 复制策略实现了 RPO<5min 的镜像同步。K8s 集群从本地 Harbor 拉镜像,网络延迟从跨机房的 30ms 降到本地 1ms,部署速度直接提升一截。这和相关文章:容器镜像优化里讲的镜像瘦身是同一思路——减少传输体积,提升部署效率。

监控指标

Harbor 暴露了 Prometheus 指标端点:

# harbor.yml 中启用 metrics
metric:
  enabled: true
  port: 9090
  path: /metrics

关键告警规则:

# Prometheus 告警规则
groups:
  - name: harbor
    rules:
      # 磁盘使用率 > 80%
      - alert: HarborStorageHigh
        expr: |
          harbor_storage_usage_bytes / harbor_storage_capacity_bytes > 0.8          
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Harbor 存储使用率超过 80%"

      # 扫描失败率 > 10%
      - alert: HarborScanFailureHigh
        expr: |
          rate(harbor_scan_total{result="error"}[5m])
          / rate(harbor_scan_total[5m]) > 0.1          
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Harbor 扫描失败率超过 10%"

      # GC 执行失败
      - alert: HarborGCFailed
        expr: harbor_gc_status{status="failed"} == 1
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Harbor 垃圾回收执行失败"

这块可以结合相关文章:Prometheus 监控体系快速搭建来配置完整的监控告警链路。

备份策略

Harbor 的核心数据有两块:PostgreSQL 里的元数据存储层的 blob 文件。备份策略:

数据类型备份方式恢复方式频率
PostgreSQLpg_dump 定时备份恢复 dump 文件每天
Blob 文件存储层快照(NFS/Ceph snapshot)回滚快照每天
harbor.ymlGit 管理git checkout变更时
# PostgreSQL 备份脚本
#!/bin/bash
DATE=$(date +%Y%m%d)
pg_dump -h harbor-db.internal -U postgres harbor | gzip > /backup/harbor-db-${DATE}.sql.gz
# 保留 30 天
find /backup -name "harbor-db-*.sql.gz" -mtime +30 -delete

别只备数据库不备 blob。元数据没了你可以重建项目结构,但 blob 文件丢了就是镜像丢了。两样都得备。

一个完整的 CI/CD 集成示例

把前面讲的串起来,看看在流水线里怎么用 Harbor:

# .gitlab-ci.yml 示例
stages:
  - build
  - push
  - scan

build_and_push:
  stage: build
  image: docker:24
  variables:
    HARBOR_URL: "harbor.yourcompany.com"
    HARBOR_USER: "ci-pusher"  # 机器人账号
    HARBOR_PASS: "$HARBOR_CI_TOKEN"  # 从 CI 变量注入
  before_script:
    # 登录 Harbor
    - echo "$HARBOR_PASS" | docker login -u "$HARBOR_USER" --password-stdin "$HARBOR_URL"
  script:
    # 构建镜像,用多阶段构建减小体积
    - docker build -t $HARBOR_URL/prod/app:$CI_COMMIT_TAG .
    # 推送前先做本地 Trivy 扫描
    - trivy image --severity HIGH,CRITICAL --exit-code 1 $HARBOR_URL/prod/app:$CI_COMMIT_TAG
    # 扫描通过才推送
    - docker push $HARBOR_URL/prod/app:$CI_COMMIT_TAG
    # Harbor 会自动触发二次扫描
  after_script:
    - docker logout $HARBOR_URL
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/  # 只有打 tag 才推送

这里有两层扫描:CI 本地 Trivy 扫一遍 + Harbor 推送后再扫一遍。看起来重复,但价值不同——CI 本地扫描是阻断式的(有问题直接 fail),Harbor 扫描是审计式的(记录漏洞全貌,供后续查询)。

我推荐 CI/CD 流水线里做本地扫描阻断,Harbor 做全量扫描记录。两层策略互补:CI 拦新镜像,Harbor 查历史镜像。

总结

制品仓库不是镜像的网盘。它的核心价值在三个维度:

  1. 权限管控:项目隔离 + RBAC 角色 + 机器人账号,让谁该推、谁该拉清清楚楚
  2. 安全扫描:Trivy 自动扫描 + 漏洞阻断策略 + CVE 白名单管理,把漏洞镜像拦在生产之前
  3. 存储治理:保留策略自动清理无用 tag + GC 回收磁盘空间 + 配额兜底,防止磁盘爆炸

从我的实践经验看,三个维度里存储治理是最容易被忽略的。很多团队装了 Harbor、配了扫描、分了项目,就是没配保留策略和 GC。三个月后磁盘告警,半夜起来手动删镜像——就是开头那个场景。

配置建议一句话总结:

  • dev 项目:保留最近 10 个 tag,每天 GC
  • staging 项目:保留 7 天内 tag,每天 GC
  • prod 项目:保留 v* 最近 20 个 tag,每周 GC
  • 所有项目:设 50-100GB 存储配额兜底
  • 生产项目:开 Trivy 自动扫描 + High 级别阻断

Harbor 本身不难装,难的是养成持续治理的习惯。镜像管理是 CI/CD 的最后一公里,这公里走不好,前面构建再快也是白搭。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. Harbor GitHub Repository — Harbor 官方团队,CNCF 毕业项目,提供镜像存储、签名、扫描的企业级 Registry
  2. Harbor 官方文档 - Garbage Collection — Harbor 官方文档,垃圾回收机制和存储管理说明
  3. Trivy 官方文档 — Aqua Security,Trivy 漏洞扫描引擎的配置和使用指南
  4. Harbor Helm Chart — Harbor 团队,Helm Chart 部署配置和参数说明
  5. CNCF Harbor 项目介绍 — CNCF 基金会,Harbor 项目概况和社区动态