一、为什么需要 Harbor
1.1 Docker Hub 的痛点
Docker Hub 是公共镜像仓库,企业大规模使用时会遇到以下问题:
- 网络限制:拉取镜像慢、不稳定,生产环境不可控
- 安全风险:公共镜像来源不明,可能包含漏洞或恶意代码
- 合规要求:金融、政务等行业要求镜像必须存储在境内或内部
- 版本管理缺失:无法按项目、团队、环境进行细粒度权限控制
- 拉取次数限制:Docker Hub 对匿名和免费账户有拉取配额
1.2 私有镜像仓库的选择
| 方案 |
定位 |
特点 |
| Docker Registry |
基础镜像存储 |
官方开源,功能单一,无 UI |
| Harbor |
企业级镜像仓库 |
图形界面、权限管理、漏洞扫描、镜像复制、签名验证 |
| Nexus |
通用制品库 |
支持 Maven、npm、Docker 等多种制品 |
| Quay / ACR / TCR |
商业/云厂商 |
开箱即用,按量付费 |
选型建议:
- 团队规模小、只需要存储镜像 → Docker Registry
- 需要权限管理、扫描、审计、多仓库同步 → Harbor
- 已经有 Nexus 管理 Java/NPM 制品 → 可直接复用 Nexus 的 Docker 仓库
- 不想自建、预算充足 → 云厂商容器镜像服务
1.3 Harbor 能解决什么问题
- 镜像私有化:将业务镜像统一存储在公司内部
- 权限隔离:按项目划分命名空间,控制谁可以推送/拉取
- 漏洞扫描:集成 Trivy,扫描镜像中的 OS/应用漏洞
- 镜像复制:跨机房、跨云同步镜像,构建多活/灾备
- 签名与可信:支持 Notation / Cosign 镜像签名,防止镜像被篡改
- 审计与合规:记录推送、拉取、删除操作日志
二、Harbor 是什么
2.1 官方定义
Harbor 是一个开源的云原生制品仓库(Cloud Native Registry),用于存储、签名和扫描容器镜像、Helm Chart、OCI Artifact 等云原生制品。
它由 VMware 开源,现为 CNCF 孵化项目,地址:https://goharbor.io
2.2 支持的制品类型
Harbor 基于 OCI(Open Container Initiative)规范,支持:
- Docker 镜像
- Helm Chart
- OCI Artifact(如 SBOM、签名包)
- CNAB(Cloud Native Application Bundle)
2.3 与 Docker Registry 的关系
Harbor 底层基于 Docker Distribution(即 Docker Registry v2)实现镜像的 blob 存储,但在此基础上增加了:
- Web UI(Portal)
- 用户与权限系统
- 数据库元数据管理
- 异步任务调度(Jobservice)
- 漏洞扫描适配器
- 多仓库复制
- 镜像签名验证
可以理解为:Harbor = Docker Registry + 企业级管理平台
三、Harbor 核心架构
3.1 数据分层模型
很多坑其实源于没想清楚 Harbor 的数据分层。它不是一个单体,而是把”元数据”和”镜像 blob”拆成了两层:
| 层级 |
组件 |
存什么 |
是否状态 |
说明 |
| 接入层 |
Nginx / Ingress |
TLS 终止、路由转发 |
无 |
无状态时可以多副本 |
| 服务层 |
Portal / Core / Jobservice |
API、UI、异步任务 |
无(可多副本) |
Core 是核心 API 网关 |
| 安全层 |
Trivy(Scanner) |
漏洞扫描 |
无 |
扫描结果元数据进 PG |
| 镜像层 |
Registry + RegistryCTL |
镜像 blob 实际存储 |
有 |
需要持久化,多副本需共享存储 |
| 元数据层 |
PostgreSQL |
项目/用户/镜像索引/审计日志 |
有 |
核心状态库,建议外部高可用 |
| 缓存层 |
Redis |
会话、Job 队列、Token |
有 |
建议外部 Redis/Sentinel |
关键理解:
- 无状态层(Portal/Core/Jobservice/Trivy)可以随时水平扩展
- 有状态层(Registry 存储、PG、Redis)必须保证数据持久化和高可用
- 生产环境尽量使用外部 PostgreSQL + 外部 Redis + 共享对象存储/SAN
3.2 核心组件职责
| 组件 |
职责 |
| nginx |
反向代理,统一入口,处理 TLS、静态资源、路由 |
| portal |
Harbor Web UI,基于 Angular 开发 |
| core |
核心业务服务,提供 REST API,处理认证、权限、项目管理、Webhook 等 |
| jobservice |
异步任务调度,负责镜像复制、垃圾回收、扫描、保留策略等 |
| registry |
基于 Docker Distribution,负责镜像 blob 的推拉 |
| registryctl |
Registry 的管理面,执行垃圾回收等操作 |
| trivy-adapter |
漏洞扫描适配器,调用 Trivy 扫描镜像 |
| postgresql |
元数据库,存储用户、项目、仓库、Artifact、审计日志等 |
| redis |
缓存会话、任务队列、镜像层缓存索引 |
| exporter |
Prometheus 指标导出器 |
3.3 请求流转示意
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Docker Client / Browser │ ▼ Nginx / Ingress ←── TLS 终止 │ ┌────┴────┐ ▼ ▼ Portal Core API │ │ │ ┌────┴────┐ │ ▼ ▼ │ Registry PostgreSQL │ │ │ │ ▼ ▼ │ 镜像 Blob Redis 缓存 │ ▼ Jobservice ──→ Trivy / 复制目标 / GC
|
四、安装 Harbor
4.1 安装前准备
4.1.1 硬件要求(生产参考)
| 规模 |
CPU |
内存 |
存储 |
说明 |
| 测试环境 |
2 核 |
4 GB |
50 GB |
单节点即可 |
| 小型生产 |
4 核 |
8 GB |
200 GB+ |
建议外部数据库 |
| 中大型生产 |
8 核+ |
16 GB+ |
1 TB+ |
必须外部 PG/Redis + 对象存储 |
4.1.2 软件依赖
- Docker 20.10+(Docker Compose 方式)
- Docker Compose v2.0+(Docker Compose 方式)
- Kubernetes 1.24+(Helm 方式)
- Helm 3.12+
- 可解析的域名(推荐)
- 有效 TLS 证书(生产环境)
4.1.3 网络规划
- 确定 Harbor 的访问域名,例如
harbor.example.com
- 确定暴露方式:Ingress / NodePort / LoadBalancer / ClusterIP
- 确定镜像推拉端口:HTTPS 默认 443,HTTP 默认 80
- 如果走 NodePort,客户端 tag 和 login 时必须带端口
4.2 方案一:Docker Compose 安装
适用于单节点测试、小型环境或快速验证。
4.2.1 下载离线安装包
1 2 3 4 5
| wget https://github.com/goharbor/harbor/releases/download/v2.15.2/harbor-offline-installer-v2.15.2.tgz
tar xzf harbor-offline-installer-v2.15.2.tgz && cd harbor
|
4.2.2 配置文件
1 2
| cp harbor.yml.tmpl harbor.yml
|
最小化配置示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| hostname: harbor.example.com
http: port: 80
https: port: 443 certificate: /your/path/harbor.crt private_key: /your/path/harbor.key
harbor_admin_password: Harbor12345
database: password: root123 max_idle_conns: 100 max_open_conns: 900
data_volume: /data
log: level: info local: rotate_count: 50 rotate_size: 200M location: /var/log/harbor
|
4.2.3 执行安装
1 2 3 4 5 6 7 8
| ./install.sh
./install.sh --with-trivy
./prepare
|
4.2.4 常用管理命令
1 2 3 4 5 6 7 8 9 10 11
| docker compose -f docker-compose.yml down
docker compose -f docker-compose.yml up -d
docker compose -f docker-compose.yml ps
docker compose -f docker-compose.yml logs -f core
|
4.3 方案二:Kubernetes Helm 安装
适用于生产环境,便于扩展、升级和高可用。
4.3.1 添加仓库
1 2 3 4 5
| helm repo add harbor https://helm.goharbor.io helm repo update
helm search repo harbor/harbor --versions | head -20
|

4.3.2 导出默认配置
1
| helm show values harbor/harbor > harbor-values.yaml
|
harbor-values.yaml内容很多,后面会有篇章讲配置解释,下面是我测试使用的一个配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
| expose: type: ingress
tls: enabled: true certSource: auto auto: commonName: "" secret: secretName: ""
ingress: hosts: core: harbor.rstyro.com controller: default className: "higress" annotations: ingress.kubernetes.io/ssl-redirect: "true" ingress.kubernetes.io/proxy-body-size: "0" nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingkubernetes.io/proxy-body-size: "0" nginx.ingress.kubernetes.io/proxy-read-timeout: "600" nginx.ingress.kubernetes.io/proxy-send-timeout: "600" labels: {}
externalURL: https://harbor.rstyro.com
caSecretName: ""
persistence: enabled: true resourcePolicy: "keep" persistentVolumeClaim: registry: existingClaim: "" storageClass: "nfs-client" subPath: "harbor/registry" accessMode: ReadWriteMany size: 50Gi jobservice: jobLog: storageClass: "nfs-client" subPath: "harbor/jobservice" accessMode: ReadWriteMany size: 5Gi trivy: storageClass: "nfs-client" subPath: "harbor/trivy" accessMode: ReadWriteMany size: 5Gi
harborAdminPassword: "Rstyro168"
logLevel: info
secretKey: "a1b2c3d4e5f6g7h8"
proxy: httpProxy: httpsProxy: noProxy: 127.0.0.1,localhost,.local,.internal components: []
metrics: enabled: false core: path: /metrics port: 8001 registry: path: /metrics port: 8001 jobservice: path: /metrics port: 8001 exporter: path: /metrics port: 8001
serviceMonitor: enabled: false additionalLabels: {} interval: "30s"
portal: image: repository: docker.io/goharbor/harbor-portal tag: v2.15.1 replicas: 1 resources: requests: memory: 256Mi cpu: 100m limits: memory: 512Mi cpu: 200m
core: image: repository: docker.io/goharbor/harbor-core tag: v2.15.1 replicas: 1 resources: requests: memory: 512Mi cpu: 200m limits: memory: 1Gi cpu: 500m
jobservice: image: repository: docker.io/goharbor/harbor-jobservice tag: v2.15.1 replicas: 1 resources: requests: memory: 512Mi cpu: 200m limits: memory: 1Gi cpu: 500m
registry: registry: image: repository: docker.io/goharbor/registry-photon tag: v2.15.1 resources: requests: memory: 256Mi cpu: 100m limits: memory: 1Gi cpu: 500m controller: image: repository: docker.io/goharbor/harbor-registryctl tag: v2.15.1 resources: requests: memory: 256Mi cpu: 100m limits: memory: 512Mi cpu: 200m replicas: 1 credentials: username: "harbor_registry_user" password: "harbor_registry_password"
trivy: enabled: true image: repository: docker.io/goharbor/trivy-adapter-photon tag: v2.15.1 replicas: 1 resources: requests: memory: 512Mi cpu: 200m limits: memory: 2Gi cpu: 1
skipUpdate: true dbRepository: - "mirror.gcr.io/aquasec/trivy-db" - "ghcr.io/aquasecurity/trivy-db" javaDBRepository: - "mirror.gcr.io/aquasec/trivy-java-db" - "ghcr.io/aquasecurity/trivy-java-db"
database: type: internal internal: image: repository: docker.io/goharbor/harbor-db tag: v2.15.1 resources: requests: memory: 512Mi cpu: 200m limits: memory: 1Gi cpu: 500m password: "rstyro"
redis: type: internal internal: image: repository: docker.io/goharbor/redis-photon tag: v2.15.1 resources: requests: memory: 256Mi cpu: 100m limits: memory: 512Mi cpu: 200m
exporter: image: repository: docker.io/goharbor/harbor-exporter tag: v2.15.1 replicas: 1 resources: requests: memory: 128Mi cpu: 50m limits: memory: 256Mi cpu: 100m
|
- 我这个存储配置使用了NFS动态分配,默认StorageClass为:
nfs-client NFS的安装与配置可参考我之前的文章
- 然后ingress 的实现使用的是Higress,也可以使用ingress-nginx只是不维护就想着换一个
- 还有tls这里使用
auto 证书有效期一年做测试够用了,后面会有篇章讲 certSource: secret 的
4.3.3 创建命名空间并安装
1 2 3 4 5
| kubectl create ns harbor
helm install harbor harbor/harbor -n harbor -f harbor-values.yaml
|
安装成功之后,还要安装ingress的控制器,ingress的控制器有很多,我上面配置使用的是higress,我们查看一下它的网关地址

可以看到,443映射30145端口,所以我们就可以通过 域名:30145 进行访问,higress会通过域名自动进行代理

账号密码就是之前配置文件里面harborAdminPassword设置的:Rstyro168

具体的仓库如何上传镜像和下载镜像看后面的章节
4.3.4 升级与卸载
1 2 3 4 5 6 7 8 9
| helm upgrade harbor harbor/harbor -n harbor -f harbor-values.yaml
helm uninstall harbor -n harbor
kubectl delete pvc -n harbor --all kubectl delete namespace harbor
|
4.4 两种方案对比
| 维度 |
Docker Compose |
Helm on Kubernetes |
| 适用场景 |
测试、单节点、小团队 |
生产、大规模、高可用 |
| 扩展性 |
垂直扩展为主 |
水平扩展、多副本 |
| 高可用 |
需要手动配置 |
原生支持 |
| 运维复杂度 |
低 |
中 |
| 存储方式 |
本地目录/NFS |
PVC / 对象存储 |
| 升级方式 |
重新执行 install.sh |
helm upgrade |
生产强烈推荐 Helm + 外部 PG/Redis + 对象存储/SAN 共享存储。
五、配置解析与生产建议
5.1 默认 values 配置总体模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885
| expose: type: ingress tls: enabled: true certSource: auto auto: commonName: "" secret: secretName: "" ingress: hosts: core: core.harbor.domain controller: default kubeVersionOverride: "" className: "" annotations: ingress.kubernetes.io/ssl-redirect: "true" ingress.kubernetes.io/proxy-body-size: "0" nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/proxy-body-size: "0" labels: {} route: labels: {} annotations: {} parentRefs: [] hosts: [] clusterIP: name: harbor staticClusterIP: "" ports: httpPort: 80 httpsPort: 443 annotations: {} labels: {} nodePort: name: harbor ports: http: port: 80 nodePort: 30002 https: port: 443 nodePort: 30003 annotations: {} labels: {} loadBalancer: name: harbor IP: "" ports: httpPort: 80 httpsPort: 443 annotations: {} labels: {} sourceRanges: []
externalURL: https://core.harbor.domain
persistence: enabled: true resourcePolicy: "keep" persistentVolumeClaim: registry: existingClaim: "" storageClass: "" subPath: "" accessMode: ReadWriteOnce size: 5Gi annotations: {} jobservice: jobLog: existingClaim: "" storageClass: "" subPath: "" accessMode: ReadWriteOnce size: 1Gi annotations: {} database: existingClaim: "" storageClass: "" subPath: "" accessMode: ReadWriteOnce size: 1Gi annotations: {} redis: existingClaim: "" storageClass: "" subPath: "" accessMode: ReadWriteOnce size: 1Gi annotations: {} trivy: existingClaim: "" storageClass: "" subPath: "" accessMode: ReadWriteOnce size: 5Gi annotations: {} imageChartStorage: disableredirect: false type: filesystem filesystem: rootdirectory: /storage azure: accountname: accountname accountkey: base64encodedaccountkey container: containername existingSecret: "" gcs: bucket: bucketname encodedkey: base64-encoded-json-key-file existingSecret: "" useWorkloadIdentity: false s3: region: us-west-1 bucket: bucketname swift: authurl: https://storage.myprovider.com/v3/auth username: username password: password container: containername existingSecret: "" oss: accesskeyid: accesskeyid accesskeysecret: accesskeysecret region: regionname bucket: bucketname existingSecret: ""
existingSecretAdminPassword: "" existingSecretAdminPasswordKey: HARBOR_ADMIN_PASSWORD harborAdminPassword: "Harbor12345"
internalTLS: enabled: false strong_ssl_ciphers: false certSource: "auto" trustCa: "" core: secretName: "" crt: "" key: "" jobservice: secretName: "" crt: "" key: "" registry: secretName: "" crt: "" key: "" portal: secretName: "" crt: "" key: "" trivy: secretName: "" crt: "" key: ""
ipFamily: ipv6: enabled: true ipv4: enabled: true policy: "" families: []
imagePullPolicy: IfNotPresent
imagePullSecrets:
updateStrategy: type: RollingUpdate
logLevel: info
caSecretName: ""
secretKey: "not-a-secure-key"
existingSecretSecretKey: ""
proxy: httpProxy: httpsProxy: noProxy: 127.0.0.1,localhost,.local,.internal components: - core - jobservice - trivy
enableMigrateHelmHook: false
metrics: enabled: false core: path: /metrics port: 8001 registry: path: /metrics port: 8001 jobservice: path: /metrics port: 8001 exporter: path: /metrics port: 8001 serviceMonitor: enabled: false additionalLabels: {} interval: "" metricRelabelings: [] relabelings: []
trace: enabled: false provider: jaeger sample_rate: 1 jaeger: endpoint: http://hostname:14268/api/traces otel: endpoint: hostname:4318 url_path: /v1/traces compression: false insecure: true timeout: 10
cache: enabled: false expireHours: 24
containerSecurityContext: privileged: false allowPrivilegeEscalation: false seccompProfile: type: RuntimeDefault runAsNonRoot: true capabilities: drop: - ALL
nginx: image: repository: docker.io/goharbor/nginx-photon tag: v2.15.1 serviceAccountName: "" automountServiceAccountToken: false replicas: 1 podDisruptionBudget: enabled: false minAvailable: 1 revisionHistoryLimit: 10 extraEnvVars: [] nodeSelector: {} tolerations: [] affinity: {} topologySpreadConstraints: [] podAnnotations: {} podLabels: {} priorityClassName: livenessProbe: initialDelaySeconds: 300 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 readinessProbe: initialDelaySeconds: 1 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1
portal: image: repository: docker.io/goharbor/harbor-portal tag: v2.15.1 serviceAccountName: "" automountServiceAccountToken: false replicas: 1 podDisruptionBudget: enabled: false minAvailable: 1 revisionHistoryLimit: 10 extraEnvVars: [] nodeSelector: {} tolerations: [] affinity: {} topologySpreadConstraints: [] podAnnotations: {} podLabels: {} serviceAnnotations: {} priorityClassName: livenessProbe: initialDelaySeconds: 300 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 readinessProbe: initialDelaySeconds: 1 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 initContainers: []
core: image: repository: docker.io/goharbor/harbor-core tag: v2.15.1 serviceAccountName: "" automountServiceAccountToken: false replicas: 1 podDisruptionBudget: enabled: false minAvailable: 1 revisionHistoryLimit: 10 startupProbe: enabled: true initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 360 successThreshold: 1 livenessProbe: initialDelaySeconds: 0 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 2 successThreshold: 1 readinessProbe: initialDelaySeconds: 0 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 2 successThreshold: 1 extraEnvVars: [] nodeSelector: {} tolerations: [] affinity: {} topologySpreadConstraints: [] podAnnotations: {} podLabels: {} serviceAnnotations: {} priorityClassName: initContainers: [] configureUserSettings: quotaUpdateProvider: db secret: "" existingSecret: "" secretName: "" tokenKey: | tokenCert: | # XSRF跨站请求伪造密钥,必须32位字符,空自动生成 xsrfKey: "" existingXsrfSecret: "" existingXsrfSecretKey: CSRF_KEY # 镜像拉取计数异步刷新间隔(秒),默认10秒 artifactPullAsyncFlushDuration: gdpr: deleteUser: false # 是否开启用户数据彻底删除 auditLogsCompliant: false # 审计日志合规模式
jobservice: image: repository: docker.io/goharbor/harbor-jobservice tag: v2.15.1 replicas: 1 podDisruptionBudget: enabled: false minAvailable: 1 revisionHistoryLimit: 10 serviceAccountName: "" automountServiceAccountToken: false livenessProbe: initialDelaySeconds: 300 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 readinessProbe: initialDelaySeconds: 20 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 extraEnvVars: [] nodeSelector: {} tolerations: [] affinity: {} topologySpreadConstraints: podAnnotations: {} podLabels: {} priorityClassName: initContainers: [] maxJobWorkers: 10 jobLoggers: - file loggerSweeperDuration: 14 notification: webhook_job_max_retry: 3 webhook_job_http_client_timeout: 3 reaper: max_update_hours: 24 max_dangling_hours: 168 secret: "" existingSecret: "" existingSecretKey: JOBSERVICE_SECRET registryHttpClientTimeout: 30
registry: registry: image: repository: docker.io/goharbor/registry-photon tag: v2.15.1 extraEnvVars: [] livenessProbe: initialDelaySeconds: 300 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 readinessProbe: initialDelaySeconds: 1 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 controller: image: repository: docker.io/goharbor/harbor-registryctl tag: v2.15.1 extraEnvVars: [] livenessProbe: initialDelaySeconds: 300 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 readinessProbe: initialDelaySeconds: 1 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 serviceAccountName: "" automountServiceAccountToken: false replicas: 1 podDisruptionBudget: enabled: false minAvailable: 1 revisionHistoryLimit: 10 nodeSelector: {} tolerations: [] affinity: {} topologySpreadConstraints: [] podAnnotations: {} podLabels: {} priorityClassName: initContainers: [] secret: "" existingSecret: "" existingSecretKey: REGISTRY_HTTP_SECRET relativeurls: false credentials: username: "harbor_registry_user" password: "harbor_registry_password" existingSecret: "" htpasswdString: "" middleware: enabled: false type: cloudFront cloudFront: baseurl: example.cloudfront.net keypairid: KEYPAIRID duration: 3000s ipfilteredby: none privateKeySecret: "my-secret" upload_purging: enabled: true age: 168h interval: 24h dryrun: false
trivy: enabled: true image: repository: docker.io/goharbor/trivy-adapter-photon tag: v2.15.1 serviceAccountName: "" automountServiceAccountToken: false replicas: 1 podDisruptionBudget: enabled: false minAvailable: 1 resources: requests: cpu: 200m memory: 512Mi limits: cpu: 1 memory: 1Gi livenessProbe: initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 10 successThreshold: 1 readinessProbe: initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 extraEnvVars: [] nodeSelector: {} tolerations: [] affinity: {} topologySpreadConstraints: [] podAnnotations: {} podLabels: {} priorityClassName: initContainers: [] debugMode: false vulnType: "os,library" severity: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL" ignoreUnfixed: false insecure: false gitHubToken: "" skipUpdate: false skipJavaDBUpdate: false dbRepository: - "mirror.gcr.io/aquasec/trivy-db" - "ghcr.io/aquasecurity/trivy-db" javaDBRepository: - "mirror.gcr.io/aquasec/trivy-java-db" - "ghcr.io/aquasecurity/trivy-java-db" offlineScan: false securityCheck: "vuln" timeout: 5m0s
database: type: internal internal: image: repository: docker.io/goharbor/harbor-db tag: v2.15.1 serviceAccountName: "" automountServiceAccountToken: false livenessProbe: initialDelaySeconds: 300 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 readinessProbe: initialDelaySeconds: 1 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 extraEnvVars: [] nodeSelector: {} tolerations: [] affinity: {} priorityClassName: extrInitContainers: [] password: "changeit" shmSizeLimit: 512Mi initContainer: migrator: {} permissions: {} external: host: "192.168.0.1" port: "5432" username: "user" password: "password" coreDatabase: "registry" existingSecret: "" sslmode: "disable" maxIdleConns: 100 maxOpenConns: 900 podAnnotations: {} podLabels: {}
redis: type: internal internal: image: repository: docker.io/goharbor/redis-photon tag: v2.15.1 serviceAccountName: "" automountServiceAccountToken: false livenessProbe: initialDelaySeconds: 300 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 readinessProbe: initialDelaySeconds: 1 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 extraEnvVars: [] nodeSelector: {} tolerations: [] affinity: {} priorityClassName: initContainers: [] jobserviceDatabaseIndex: "1" registryDatabaseIndex: "2" trivyAdapterIndex: "5" external: addr: "192.168.0.2:6379" sentinelMasterSet: "" tlsOptions: enable: false coreDatabaseIndex: "0" jobserviceDatabaseIndex: "1" registryDatabaseIndex: "2" trivyAdapterIndex: "5" username: "" password: "" existingSecret: "" podAnnotations: {} podLabels: {}
exporter: image: repository: docker.io/goharbor/harbor-exporter tag: v2.15.1 serviceAccountName: "" automountServiceAccountToken: false replicas: 1 podDisruptionBudget: enabled: false minAvailable: 1 revisionHistoryLimit: 10 livenessProbe: initialDelaySeconds: 300 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 readinessProbe: initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1 extraEnvVars: [] podAnnotations: {} podLabels: {} nodeSelector: {} tolerations: [] affinity: {} topologySpreadConstraints: [] priorityClassName: cacheDuration: 23 cacheCleanInterval: 14400
|
5.2 TLS 与证书配置详解
TLS 是 Harbor 最容易出问题的环节。理解证书信任链是排查 401、x509 错误的关键。
5.2.1 TLS 核心配置块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| expose: type: ingress tls: enabled: true certSource: secret auto: commonName: "" secret: secretName: "harbor-rstyro-tls" ingress: hosts: core: harbor.rstyro.com className: "higress" annotations: ingress.kubernetes.io/ssl-redirect: "true" ingress.kubernetes.io/proxy-body-size: "0" nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingkubernetes.io/proxy-body-size: "0" nginx.ingress.kubernetes.io/proxy-read-timeout: "600" nginx.ingress.kubernetes.io/proxy-send-timeout: "600" externalURL: https://harbor.rstyro.com
|
| 字段 |
说明 |
enabled |
是否启用 HTTPS。生产必须 true |
certSource |
证书来源:auto 自签、secret 自有证书、none 使用 Ingress 默认证书 |
auto.commonName |
非 Ingress 模式必填域名 |
secret.secretName |
certSource: secret 时,指定 TLS Secret 名称 |
5.2.2 配套全局关联配置
1 2 3 4 5 6
| externalURL: https://harbor.rstyro.com
expose: ingress: hosts: core: harbor.rstyro.com
|
注意:externalURL 和 ingress.hosts.core 必须完全一致,否则:
- 浏览器报证书域名不匹配
- Docker login 报 401 鉴权失败
- Token 服务返回错误地址
5.2.3 模式一:certSource = auto(自动生成证书)
适用场景:测试、内网临时环境、快速验证。
1 2 3 4 5 6 7 8 9 10
| expose: type: ingress tls: enabled: true certSource: auto ingress: hosts: core: harbor.rstyro.com
externalURL: https://harbor.rstyro.com
|
优点:
- 零证书准备,一条命令部署
- 自动创建 TLS Secret 并绑定 Ingress
缺点:
- 自签证书不被浏览器/客户端信任
- Docker login 会报
x509: certificate signed by unknown authority
- 升级 Harbor 可能重新生成证书,导致客户端信任失效
- 企业等保、合规场景不认可
5.2.4 模式二:certSource = secret(企业生产标准)
支持三种证书来源:
- 公网可信 SSL(Let’s Encrypt、云厂商付费证书)
- 企业自建 OpenCA 签发的内网域名证书
- cert-manager 自动签发
配置示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| expose: type: ingress tls: enabled: true certSource: secret secret: secretName: "harbor-tls" ingress: hosts: core: harbor.rstyro.com
externalURL: https://harbor.rstyro.com
caSecretName: "harbor-root-ca"
|
前置要求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
cat domain.crt chain.crt > fullchain.crt
kubectl create secret tls harbor-tls \ -n harbor \ --cert ./fullchain.crt \ --key ./domain.key
kubectl create secret generic harbor-root-ca \ -n harbor \ --from-file=ca.crt=./ca.crt
|
证书文件要求:
tls.crt:完整证书链,顺序为 域名证书 → 中间 CA 证书(部分场景需包含根 CA)
tls.key:无密码的 RSA/ECC 私钥
- SAN/CN 必须包含
harbor.example.com
5.2.5 自建 CA 签发证书完整流程
适用于企业内网没有公网域名的场景。
- 示例需要配置域名:harbor.rstyro.com
步骤 1:创建根 CA
1 2 3 4 5 6 7
| openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \ -subj "/C=CN/ST=Guangdong/L=Shenzhen/O=Company/OU=DevOps/CN=Company-Internal-CA" \ -out ca.crt
|
步骤 2:创建域名证书请求并签发
创建 v3.ext 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| [req] distinguished_name = dn x509_extensions = v3 prompt = no
[dn] C=CN ST=Guangdong L=Shenzhen O=Company OU=DevOps CN=harbor.rstyro.com
[v3] keyUsage = digitalSignature, keyEncipherment extendedKeyUsage = serverAuth subjectAltName = @alt
[alt] DNS.1=harbor.rstyro.com
|
签发:
1 2 3 4 5 6 7 8 9 10
| openssl genrsa -out harbor.key 2048
openssl req -new -key harbor.key -out harbor.csr -config v3.ext
openssl x509 -req -in harbor.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out harbor.crt -days 1095 -sha256 -extfile v3.ext -extensions v3
|
步骤 3:创建 K8s Secret
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| cat harbor.crt ca.crt > fullchain.crt
kubectl create secret tls harbor-tls -n harbor --cert fullchain.crt --key harbor.key
kubectl create secret generic harbor-root-ca -n harbor --from-file=ca.crt=ca.crt
kubectl get secret harbor-root-ca -n harbor
kubectl get ingress -n harbor -o yaml | grep secretName
echo | openssl s_client -connect harbor.rstyro.com:30145 -servername harbor.rstyro.com 2>/dev/null | openssl x509 -noout -text | grep -E "Not Before|Not After|Issuer:"
|
5.2.6 客户端信任自建 CA
服务器端配置完成后,客户端(Docker/containerd/nerdctl)也需要信任该 CA:
1 2 3 4 5 6 7 8 9
| cp ca.crt /usr/local/share/ca-certificates/harbor-ca.crt update-ca-certificates
cp ca.crt /etc/containerd/certs.d/harbor.rstyro.com/ca.crt
systemctl restart containerd
|
5.3 使用cert-manager自动管理证书
cert-manager 是 Kubernetes 上最常用的证书自动化管理工具,支持 Let’s Encrypt、ACME、私有 CA 等多种 Issuer。与 Harbor 配合后,可以实现证书自动申请、自动续期、自动更新 Secret,无需人工干预。
适用场景
- 使用公网域名(如
harbor.example.com)
- 集群已安装 cert-manager
- 有可用的 Issuer:
ClusterIssuer 或 NamespaceIssuer
- 希望证书到期前自动续期
步骤 1:安装 cert-manager(如未安装)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| helm install \ cert-manager oci://quay.io/jetstack/charts/cert-manager \ --namespace cert-manager \ --create-namespace \ --version v1.21.0 \ --set crds.enabled=true
helm uninstall cert-manager --namespace cert-manager
kubectl delete crd \ issuers.cert-manager.io \ clusterissuers.cert-manager.io \ certificates.cert-manager.io \ certificaterequests.cert-manager.io \ orders.acme.cert-manager.io \ challenges.acme.cert-manager.io
|
步骤 2:创建 ClusterIssuer(以 Let’s Encrypt 为例)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: admin@example.com privateKeySecretRef: name: letsencrypt-prod solvers: - http01: ingress: class: nginx
|
如果是公司自建 CA(参考 5.2.5 自建 CA 签发证书完整流程),可改用 Issuer + CA 类型,复用已有的根 CA 证书:
1 2 3 4 5
| kubectl create secret tls company-root-ca-secret \ -n cert-manager \ --cert ./ca.crt \ --key ./ca.key
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: rstyro-internal-ca spec: ca: secretName: company-root-ca-secret
cat > ca-clusterissuer.yaml<<EOF apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: rstyro-internal-ca spec: ca: secretName: company-root-ca-secret EOF
|
步骤 3:创建 Certificate 资源
在 Harbor 命名空间创建 Certificate,cert-manager 会自动签发并写入 Secret:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| cat > ca-certificate.yaml<<EOF apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: harbor-tls namespace: harbor spec: secretName: harbor-tls issuerRef: name: rstyro-internal-ca kind: ClusterIssuer dnsNames: - harbor.rstyro.com duration: 8760h renewBefore: 720h EOF
|
创建后检查证书状态:
1 2 3 4 5
| kubectl get certificate -n harbor kubectl describe certificate harbor-tls -n harbor
kubectl delete secret harbor-tls -n harbor
|
正常状态应为 Ready=True,并自动创建 harbor-tls Secret。
步骤 4:Harbor values 使用 cert-manager 生成的 Secret(如果之前secretName不一样的话)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| expose: type: ingress tls: enabled: true certSource: secret secret: secretName: "harbor-tls" ingress: hosts: core: harbor.rstyro.com className: nginx annotations: ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/proxy-body-size: "0"
externalURL: https://harbor.rstyro.com
|
然后升级 Harbor:
1 2
| helm upgrade harbor harbor/harbor -n harbor -f harbor-values.yaml
|
步骤 5:验证 cert-manager 接管成功
1 2 3 4 5 6 7 8 9
| kubectl get certificate -n harbor
kubectl get secret harbor-tls -n harbor
echo | openssl s_client -connect harbor.rstyro.com:30145 -servername harbor.rstyro.com 2>/dev/null \ | openssl x509 -noout -text | grep -E "Issuer:|Not After|DNS:"
|

自动续期机制
cert-manager 默认在证书有效期剩余 1/3 时自动续期。例如 90 天有效期的 Let’s Encrypt 证书,会在到期前 30 天左右自动:
- 向 ACME/CA 申请新证书
- 更新
harbor-rstyro-tls Secret 中的 tls.crt 和 tls.key
- Ingress 控制器 watch 到 Secret 变化后自动热加载
可以通过以下命令查看下次续期时间:
1
| kubectl get certificate harbor-tls -n harbor -o wide
|
注意事项
- 使用 Let’s Encrypt 时,域名必须能从公网访问并通过 HTTP-01 或 DNS-01 挑战
- 内网域名建议使用 DNS-01 挑战,或部署私有 ACME(如 step-ca)
- 使用私有 CA 时,客户端仍需手动信任根 CA
- 不要同时用 Helm 自动证书(
certSource: auto)和 cert-manager,两者会冲突
- 建议设置 Prometheus 告警监控
Certificate 资源的 Ready=False 状态
自建 CA 续期自动化补充
如果使用公司自建 CA,也可以将续期脚本与 cert-manager 结合:
- 编写一个 CronJob,定期调用内部 CA 接口签发新证书
- 签发后将
tls.crt 和 tls.key 写入 harbor-rstyro-tls Secret
- 或者实现自定义 cert-manager
Issuer/ClusterIssuer,对接公司 CA
更简单的方式是使用 step-ca 作为内部 ACME 服务器,cert-manager 直接通过 ACME 协议申请和续期。
5.4 外部数据库与 Redis
生产环境建议将 PG 和 Redis 外置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| database: type: external external: host: "192.168.0.10" port: "5432" username: "harbor" password: "password" coreDatabase: "registry" sslmode: "require"
redis: type: external external: addr: "192.168.0.11:6379" coreDatabaseIndex: "0" jobserviceDatabaseIndex: "1" registryDatabaseIndex: "2" trivyAdapterIndex: "5"
|
六、客户端配置与镜像操作
6.1 常用客户端工具
| 工具 |
适用场景 |
| docker |
传统 Docker 环境 |
| nerdctl |
containerd 环境(K8s 节点常用) |
| crictl |
仅用于排查,不推荐日常推送拉取 |
| helm |
推送/拉取 Helm Chart |
| oras |
推送/拉取 OCI Artifact |
6.2 配置 containerd / nerdctl 访问 Harbor
6.2.1 方式一:配置 hosts.toml 跳过 TLS 校验(测试)
1 2 3 4 5 6 7 8 9 10 11 12
| mkdir -p /etc/containerd/certs.d/harbor.rstyro.com:30145
cat > /etc/containerd/certs.d/harbor.rstyro.com:30145/hosts.toml << EOF server = "https://harbor.rstyro.com:30145"
[host."https://harbor.rstyro.com:30145"] capabilities = ["pull", "push", "resolve"] skip_verify = true EOF
systemctl restart containerd nerdctl login harbor.rstyro.com
|
6.2.2 方式二:配置 CA 证书信任(生产)
1 2 3 4 5 6 7 8 9 10
| kubectl get secret -n harbor harbor-tls \ -o jsonpath='{.data.ca\.crt}' | base64 -d > /etc/containerd/certs.d/harbor.rstyro.com:30145/ca.crt
cp /etc/containerd/certs.d/harbor.rstyro.com:30145/ca.crt /usr/local/share/ca-certificates/harbor-ca.crt update-ca-certificates
systemctl restart containerd nerdctl login harbor.rstyro.com:30145
|
6.2.3 Docker 配置 insecure registry(不推荐生产使用)
1 2 3
| { "insecure-registries": ["harbor.rstyro.com:30145"] }
|
6.3 镜像推送与拉取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| nerdctl login harbor.rstyro.com:30145
nerdctl pull nginx:alpine
nerdctl tag nginx:alpine harbor.rstyro.com:30145/library/nginx:alpine
nerdctl push harbor.rstyro.com:30145/library/nginx:alpine
nerdctl pull harbor.rstyro.com:30145/library/nginx:alpine
nerdctl rmi harbor.rstyro.com:30145/library/nginx:alpine
|

6.4 推送 Helm Chart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
helm pull harbor/harbor --untar
helm registry login harbor.rstyro.com:30145
helm package ./harbor
helm push harbor-1.19.1.tgz oci://harbor.rstyro.com:30145/library
helm pull oci://harbor.rstyro.com:30145/library/harbor --version 1.19.1
|

七、Harbor 日常使用
7.1 核心概念:项目、仓库、Artifact
| 概念 |
说明 |
类比 |
| 项目(Project) |
镜像/Chart 的命名空间,用于权限隔离 |
GitLab Group |
| 仓库(Repository) |
同一镜像/Chart 的不同版本集合 |
GitLab Project |
| Artifact |
单个镜像或 Chart 版本 |
Git Commit |
| Tag |
Artifact 的可读标识 |
Git Tag |
| Digest |
Artifact 的内容寻址哈希 |
Git SHA |
示例:harbor.rstyro.com:30145/library/nginx:alpine
library 是项目
nginx 是仓库
alpine 是 tag
7.2 项目规划建议
| 项目类型 |
用途 |
访问控制 |
library |
基础镜像(nginx、alpine、busybox) |
公开只读,管理员推送 |
middleware |
中间件镜像(redis、mysql、kafka) |
团队只读,运维推送 |
business/<team> |
业务镜像 |
团队读写,其他团队只读或无权限 |
public |
允许外部拉取的镜像 |
公开只读 |
sandbox |
测试/实验镜像 |
团队成员读写 |
7.3 用户与权限模型
Harbor 支持三类账户:
- 本地用户:Harbor 内置数据库用户
- LDAP/AD 用户:对接企业目录服务
- OIDC 用户:对接 OIDC 身份提供商(如 Keycloak、Dex)
7.3.1 项目角色
| 角色 |
权限 |
| 项目管理员(Project Admin) |
管理项目成员、配置、策略、推送/拉取/删除 |
| 维护人员(Maintainer) |
推送/拉取/删除镜像、管理标签、扫描 |
| 开发人员(Developer) |
推送/拉取镜像 |
| 访客(Guest) |
只能拉取 |
| 受限访客(Limited Guest) |
只能拉取,看不到日志和扫描结果 |
7.3.2 系统角色
| 角色 |
权限 |
| Harbor 系统管理员 |
管理所有项目、用户、配置、执行系统级操作 |
| 普通用户 |
只能访问有权限的项目 |
7.4 镜像清理策略
7.4.1 保留策略(Tag Retention)
按规则自动保留/删除 Tag,避免仓库无限膨胀。
常见规则:
- 保留最近推送的 10 个 Tag
- 保留最近 30 天内推送的 Tag
- 删除匹配
*-snapshot-* 的 Tag
- 永远保留匹配
release-* 的 Tag
7.4.2 不可变规则(Tag Immutability)
防止重要 Tag 被覆盖或删除,例如 latest、v1.0.0、release-*。
7.4.3 垃圾回收(Garbage Collection)
删除不再被任何 Tag 引用的 blob,释放存储空间。
操作路径:系统管理 → 垃圾回收 → 立即执行
注意:
- GC 会锁定 Registry,执行期间禁止推送
- 大仓库 GC 可能耗时较长,建议低峰期执行
- 可以配置定时任务自动执行
7.5 镜像复制(Replication)
Harbor 支持将镜像从一个 Harbor 实例复制到另一个 Harbor,或复制到 Docker Hub、ACR 等外部仓库。
7.5.1 典型场景
- 多地域部署,就近拉取镜像
- 生产/测试环境镜像同步
- 灾备备份
- 向公网仓库发布镜像
7.5.2 配置步骤
- 创建目标 Registry:系统管理 → 仓库管理 → 新建目标
- 创建复制规则:系统管理 → 复制管理 → 新建规则
- 选择源项目/仓库、目标仓库、触发方式
- 支持手动触发、事件触发(推送时自动复制)、定时触发
7.5.3 复制过滤器
- 按项目名称过滤
- 按 Tag 过滤(支持通配符)
- 按标签/资源类型过滤(镜像/Chart)
7.6 漏洞扫描
Harbor 默认集成 Trivy,可以对镜像执行安全扫描。
7.6.1 手动扫描
在仓库页面选择一个 Artifact,点击”扫描”。
7.6.2 自动扫描
在项目配置中开启”推送时自动扫描”。
7.6.3 扫描结果解读
| 级别 |
说明 |
| CRITICAL |
严重漏洞,建议立即修复 |
| HIGH |
高危漏洞,建议尽快修复 |
| MEDIUM |
中危漏洞,安排修复 |
| LOW |
低危漏洞,视情况处理 |
| UNKNOWN |
信息不足 |
7.6.4 扫描器配置
1 2 3 4 5 6 7 8 9 10 11
| trivy: enabled: true vulnType: "os,library" severity: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL" skipUpdate: false offlineScan: false timeout: 5m resources: limits: cpu: 1 memory: 2Gi
|
7.7 Webhook 与事件通知
Harbor 支持在镜像推送、拉取、删除、扫描完成等事件时发送 Webhook。
常见用途:
- CI/CD 触发部署
- 企业微信/钉钉/飞书通知
- 安全事件告警
- 审计日志同步
八、安全与合规
8.1 镜像签名与验证
镜像签名可以确保镜像在传输和存储过程中未被篡改。
Harbor 支持两种签名方案:
| 方案 |
工具 |
特点 |
| Notation |
notation CLI |
CNCF 推荐,兼容 OCI 1.0 |
| Cosign |
cosign |
Sigstore 生态,支持密钥less 签名 |
8.1.1 Notation 签名流程
1 2 3 4 5 6 7 8 9 10 11
| notation cert generate-test --default "harbor-signing"
notation login harbor.rstyro.com:30145
notation sign harbor.rstyro.com:30145/library/nginx:alpine
notation verify harbor.rstyro.com:30145/library/nginx:alpine
|
8.1.2 在 Harbor 中启用签名策略
可以在项目中配置”部署安全”策略:
- 仅允许签名的镜像
- 仅允许来自特定 CVE 级别的镜像
- 阻止存在 CRITICAL 漏洞的镜像拉取
8.2 访问控制最佳实践
- 禁止使用 admin 账户日常操作
- 为每个团队/项目分配独立项目
- 最小权限原则:只给需要的角色
- 定期审计用户和机器人账户
- 对接 LDAP/OIDC 实现统一身份管理
8.3 机器人账户(Robot Account)
机器人账户用于 CI/CD 流水线推送镜像,避免使用个人账号。
创建路径:项目 → 机器人账户 → 新建
建议:
- 按流水线/项目创建独立机器人账户
- 只分配 Developer 或 Maintainer 权限
- 设置过期时间
- 定期轮换 Token
8.4 审计日志
Harbor 记录以下操作:
- 用户登录/登出
- 镜像推送/拉取/删除
- 项目创建/删除
- 成员权限变更
- 配置修改
注意:审计日志存储在 PostgreSQL 中,长期运行会占用大量空间,建议定期归档或外接审计系统。
九、高可用与扩展
9.1 高可用架构设计
生产 Harbor 高可用需要考虑三个层面的冗余:
| 层面 |
高可用方案 |
| 无状态服务 |
Portal/Core/Jobservice 多副本 + K8s Deployment |
| 数据库 |
外部 PostgreSQL 主从/集群(如 Patroni、RDS、Cloud SQL) |
| 缓存 |
外部 Redis Sentinel/Cluster |
| 镜像存储 |
对象存储(S3/OSS/GCS/MinIO)或共享 SAN |
| 入口 |
Ingress 多副本 + LB |
9.2 不推荐的高可用误区
- 内置 PG/Redis 多副本 + NFS 共享:内置 PG 不支持多实例共享存储,会导致数据损坏
- Registry 使用 ReadWriteOnce PVC 多副本:Pod 无法同时挂载
- 单节点对象存储网关:对象存储本身也要高可用
9.3 性能优化
1 2 3
| cache: enabled: true expireHours: 24
|
- Registry 多副本 + 对象存储:分散拉取压力
- CDN 加速:大镜像分发时在前端加 CDN
- 配额限制:防止单个项目占用全部存储
- 镜像精简:使用多阶段构建,减少层数
9.4 容量规划
| 场景 |
估算 |
| 小型团队(10-50 镜像) |
50-100 GB |
| 中型企业(数百镜像) |
500 GB - 2 TB |
| 大型企业/多地域 |
5 TB+,使用对象存储 |
十、运维与故障排查
10.1 常用排查命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| kubectl get pods -n harbor
kubectl logs -n harbor deployment/harbor-core -f
kubectl logs -n harbor deployment/harbor-registry -c registry -f
kubectl exec -it -n harbor deployment/harbor-core -- sh
kubectl logs -n harbor deployment/harbor-jobservice -f
|
10.2 常见问题与解决方案
问题 1:Docker login 报 x509: certificate signed by unknown authority
原因:客户端不信任 Harbor 的 CA。
解决:
- 将 CA 证书加入系统信任
- containerd 配置
certs.d/<domain>/ca.crt
- 或使用
--insecure-registry(仅限测试)
问题 2:推送镜像报 unauthorized: authentication required
原因:
- 未登录或 Token 过期
- 用户没有该项目的推送权限
- 项目不存在且用户没有创建项目权限
解决:
- 执行
docker login
- 检查项目权限
- 确认项目已创建
问题 3:页面能打开,但 Docker pull 报 401
原因:
externalURL 与 Ingress 域名不一致
- Core 返回的 Token 服务地址错误
解决:
- 检查
externalURL 配置
- 检查 Ingress/Service 是否正确路由到 Core
问题 4:GC 执行后空间没有释放
原因:
- GC 设置为 dryrun 模式
- 镜像 Tag 仍被引用
- 底层存储未真正删除(对象存储需检查生命周期策略)
解决:
- 关闭 dryrun
- 删除不再需要的 Tag 后再执行 GC
- 检查对象存储侧是否有删除延迟
问题 5:Trivy 扫描失败或超时
原因:
- 无法下载漏洞库(离线/网络问题)
- 镜像过大或层数过多
- 资源限制过低
解决:
- 离线环境设置
skipUpdate: true 并预置漏洞库
- 增加
timeout
- 增加 Trivy 的 CPU/内存限制
10.3 备份与恢复
10.3.1 需要备份的数据
| 数据 |
备份方式 |
| PostgreSQL 数据库 |
pg_dump / 数据库备份工具 |
| Registry 镜像 blob |
对象存储跨区域复制 / 文件系统快照 |
| Redis |
可重建,通常不需要持久化备份 |
| Harbor 配置文件 |
Git 版本化 |
| TLS 证书 Secret |
Vault / 安全存储 |
10.3.2 数据库备份脚本示例
1 2 3
| kubectl exec -it -n harbor deployment/harbor-database -- bash -c \ "pg_dump -U postgres registry" > harbor_registry_$(date +%F).sql
|
10.3.3 恢复流程
- 在新环境部署 Harbor(使用相同版本或兼容版本)
- 恢复 PostgreSQL 数据库
- 挂载原有的 Registry 存储
- 验证登录、推送、拉取
- 执行一次 GC 清理孤儿 blob