Harbor 是一个 CNCF 基金会托管的开源的可信的云原生 docker registry 项目,可以用于存储、签名、扫描镜像内容,Harbor 通过添加一些常用的功能如安全性、身份权限管理等来扩展 docker registry 项目,此外还支持在 registry 之间复制镜像,还提供更加高级的安全功能,如用户管理、访问控制和活动审计等,在新版本中还添加了 Helm 仓库托管的支持。


docker registrydocker registry v2

Harbor 认证原理

docker registry v2docker login https://registry.qikqiak.com
  1. docker client 接收到用户输入的 docker login 命令,将命令转化为调用 engine api 的 RegistryLogin 方法
  2. 在 RegistryLogin 方法中通过 http 调用 registry 服务中的 auth 方法
  3. 因为我们这里使用的是 v2 版本的服务,所以会调用 loginV2 方法,在 loginV2 方法中会进行 /v2/ 接口调用,该接口会对请求进行认证
  4. 此时的请求中并没有包含 token 信息,认证会失败,返回 401 错误,同时会在 header 中返回去哪里请求认证的服务器地址
  5. registry client 端收到上面的返回结果后,便会去返回的认证服务器那里进行认证请求,向认证服务器发送的请求的 header 中包含有加密的用户名和密码
  6. 认证服务器从 header 中获取到加密的用户名和密码,这个时候就可以结合实际的认证系统进行认证了,比如从数据库中查询用户认证信息或者对接 ldap 服务进行认证校验
  7. 认证成功后,会返回一个 token 信息,client 端会拿着返回的 token 再次向 registry 服务发送请求,这次需要带上得到的 token,请求验证成功,返回状态码就是200了
  8. docker client 端接收到返回的200状态码,说明操作成功,在控制台上打印 Login Succeeded 的信息 至此,整个登录过程完成,整个过程可以用下面的流程图来说明:

要完成上面的登录认证过程有两个关键点需要注意:怎样让 registry 服务知道服务认证地址?我们自己提供的认证服务生成的 token 为什么 registry 就能够识别?

对于第一个问题,比较好解决,registry 服务本身就提供了一个配置文件,可以在启动 registry 服务的配置文件中指定上认证服务地址即可,其中有如下这样的一段配置信息:

......
auth:
  token:
    realm: token-realm
    service: token-service
    issuer: registry-token-issuer
    rootcertbundle: /root/certs/bundle
......

其中 realm 就可以用来指定一个认证服务的地址,下面我们可以看到 Harbor 中该配置的内容。

关于 registry 的配置,可以参考官方文档:https://docs.docker.com/registry/configuration/
JWT(JSON Web Token)

对 golang 熟悉的同学可以去 clone 下 Harbor 的代码查看下,Harbor 采用 beego 这个 web 开发框架,源码阅读起来不是特别困难。我们可以很容易看到 Harbor 中关于上面我们讲解的认证服务部分的实现方法。

安装

Harbor 涉及的组件比较多,我们可以使用 Helm 来安装一个高可用版本的 Harbor,也符合生产环境的部署方式。在安装高可用的版本之前,我们需要如下先决条件:

  • Kubernetes 集群 1.10+ 版本
  • Helm 2.8.0+ 版本
  • 高可用的 Ingress 控制器
  • 高可用的 PostgreSQL 9.6+(Harbor 不进行数据库 HA 的部署)
  • 高可用的 Redis 服务(Harbor 不处理)
  • 可以跨节点或外部对象存储共享的 PVC

Harbor 的大部分组件都是无状态的,所以我们可以简单增加 Pod 的副本,保证组件尽量分布到多个节点上即可,在存储层,需要我们自行提供高可用的 PostgreSQL、Redis 集群来存储应用数据,以及存储镜像和 Helm Chart 的 PVC 或对象存储。

首先添加 Chart 仓库地址:

# 添加 Chart 仓库
$ helm repo add harbor https://helm.goharbor.io
# 更新
$ helm repo update
# 拉取1.9.2版本并解压
$ helm pull harbor/harbor --untar --version 1.9.2
--setvalues.yaml
expose.ingress.hosts.coreexpose.ingress.hosts.notaryexternalURLdatabase.typeexternaldatabase.externalHarbor coreNotary serverNotary signerredis.typeexternalredis.externalSentinelsentinel_master_set:,:,:StorageClassStorageClasspersistence.persistentVolumeClaim.registry.storageClasspersistence.persistentVolumeClaim.chartmuseum.storageClasspersistence.persistentVolumeClaim.jobservice.storageClassReadWriteManyexistingClaimpersistence.imageChartStorage.typejobservice.jobLoggerdatabaseportal.replicascore.replicasjobservice.replicasregistry.replicaschartmuseum.replicasnotary.server.replicasnotary.signer.replicas
harbor.k8s.localnfs-clientharbornotary_servernotary_signer
$ kubectl get pods -n kube-ops -l name=postgresql
NAME                          READY   STATUS    RESTARTS   AGE
postgresql-75b8447fb5-th6bw   1/1     Running   1          2d
$ kubectl exec -it postgresql-75b8447fb5-th6bw /bin/bash -n kube-ops
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
root@postgresql-75b8447fb5-th6bw:/var/lib/postgresql# sudo su - postgres
postgres@postgresql-75b8447fb5-th6bw:~$ psql
psql (12.3 (Ubuntu 12.3-1.pgdg18.04+1))
Type "help" for help.

postgres=# CREATE DATABASE harbor OWNER postgres;
CREATE DATABASE
postgres=# GRANT ALL PRIVILEGES ON DATABASE harbor to postgres;
GRANT
postgres=# GRANT ALL PRIVILEGES ON DATABASE harbor to gitlab;
GRANT
# Todo: 用同样的方式创建其他两个数据库:notary_server、notary_signer
......
postgres-# \q  # 退出

数据库准备过后,就可以使用我们自己定制的 values 文件来进行安装了,完整的定制的 values 文件如下所示:

# values-prod.yaml
externalURL: https://harbor.k8s.local
harborAdminPassword: Harbor12345
logLevel: debug

expose:
  type: ingress
  tls:
    enabled: true
  ingress:
    className: nginx  # 指定 ingress class
    hosts:
      core: harbor.k8s.local
      notary: notary.k8s.local

persistence:
  enabled: true
  resourcePolicy: "keep"
  persistentVolumeClaim:
    registry:
      # 如果需要做高可用,多个副本的组件则需要使用支持 ReadWriteMany 的后端
      # 这里我们使用nfs,生产环境不建议使用nfs
      storageClass: "nfs-client"
      # 如果是高可用的,多个副本组件需要使用 ReadWriteMany,默认为 ReadWriteOnce
      accessMode: ReadWriteMany
      size: 5Gi
    chartmuseum:
      storageClass: "nfs-client"
      accessMode: ReadWriteMany
      size: 5Gi
    jobservice:
      storageClass: "nfs-client"
      accessMode: ReadWriteMany
      size: 1Gi
    trivy:
      storageClass: "nfs-client"
      accessMode: ReadWriteMany
      size: 2Gi

database:
  type: external
  external:
    host: "postgresql.kube-ops.svc.cluster.local"
    port: "5432"
    username: "gitlab"
    password: "passw0rd"
    coreDatabase: "harbor"
    notaryServerDatabase: "notary_server"
    notarySignerDatabase: "notary_signer"

redis:
  type: external
  external:
    addr: "redis.kube-ops.svc.cluster.local:6379"

# 默认为一个副本,如果要做高可用,只需要设置为 replicas >= 2 即可
portal:
  replicas: 1
core:
  replicas: 1
jobservice:
  replicas: 1
registry:
  replicas: 1
chartmuseum:
  replicas: 1
trivy:
  replicas: 1
notary:
  server:
    replicas: 1
  signer:
    replicas: 1

这些配置信息都是根据 Harbor 的 Chart 包默认的 values 值进行覆盖的,现在我们直接安装即可:

$ cd harbor
$ helm upgrade --install harbor . -f values-prod.yaml -n kube-ops
Release "harbor" does not exist. Installing it now.
NAME: harbor
LAST DEPLOYED: Thu Jul  7 17:31:43 2022
NAMESPACE: kube-ops
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Please wait for several minutes for Harbor deployment to complete.
Then you should be able to visit the Harbor portal at https://harbor.k8s.local
For more details, please visit https://github.com/goharbor/harbor

正常情况下隔一会儿就可以安装成功了:

$ helm ls -n kube-ops
NAME    NAMESPACE       REVISION        UPDATED                                 STATUS          CHART           APP VERSION
harbor  kube-ops        1               2022-07-07 17:31:43.083547 +0800 CST    deployed        harbor-1.9.2    2.5.2
$ kubectl get pods -n kube-ops -l app=harbor
NAME                                    READY   STATUS    RESTARTS      AGE
harbor-chartmuseum-544ddbcb64-nvk7w     1/1     Running   0             30m
harbor-core-7fd9964685-lqqw2            1/1     Running   0             30m
harbor-jobservice-6dbd89c59-vvzx5       1/1     Running   0             30m
harbor-notary-server-764b8859bf-82f5q   1/1     Running   0             30m
harbor-notary-signer-869d9bf585-kbwwg   1/1     Running   0             30m
harbor-portal-74db6bb688-2w79p          1/1     Running   0             35m
harbor-registry-695db89bfd-v9wwt        2/2     Running   0             30m
harbor-trivy-0                          1/1     Running   0             35m
harbor.k8s.local
$ kubectl get ingress -n kube-ops
NAME                                      CLASS         HOSTS                                               ADDRESS         PORTS     AGE
harbor-ingress                            nginx         harbor.k8s.local                                                    80, 443   12s
harbor-ingress-notary                     nginx         notary.k8s.local                                                    80, 443   12s
Harbor12345

登录过后即可进入 Harbor 的 Dashboard 页面:

libraryHelm Chart

推送镜像

接下来我们来测试下如何在 containerd 中使用 Harbor 镜像仓库。

/etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".registry]
  [plugins."io.containerd.grpc.v1.cri".registry.mirrors]
    [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
      endpoint = ["https://bqr1dr1n.mirror.aliyuncs.com"]
  [plugins."io.containerd.grpc.v1.cri".registry.configs]
    [plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.k8s.local".tls]
      insecure_skip_verify = true
    [plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.k8s.local".auth]
      username = "admin"
      password = "Harbor12345"
plugins."io.containerd.grpc.v1.cri".registry.configsharbor.k8s.localinsecure_skip_verify = trueplugins."io.containerd.grpc.v1.cri".registry.configs."harbor.k8s.local".auth

配置完成后重启 containerd:

$ systemctl restart containerd
nerdctl
$ nerdctl login -u admin harbor.k8s.local
Enter Password:
ERRO[0004] failed to call tryLoginWithRegHost            error="failed to call rh.Client.Do: Get \"https://harbor.k8s.local/v2/\": x509: certificate signed by unknown authority" i=0
FATA[0004] failed to call rh.Client.Do: Get "https://harbor.k8s.local/v2/": x509: certificate signed by unknown authority
[root@master1 ~]#
--insecure-registry
$ nerdctl login -u admin --insecure-registry harbor.k8s.local
Enter Password:
WARN[0004] skipping verifying HTTPS certs for "harbor.k8s.local"
WARNING: Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

然后我们先随便拉一个镜像:

$ nerdctl pull busybox:1.35.0
docker.io/library/busybox:1.35.0:                                                 resolved       |++++++++++++++++++++++++++++++++++++++|
index-sha256:8c40df61d40166f5791f44b3d90b77b4c7f59ed39a992fd9046886d3126ffa68:    done           |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:8cde9b8065696b65d7b7ffaefbab0262d47a5a9852bfd849799559d296d2e0cd: done           |++++++++++++++++++++++++++++++++++++++|
config-sha256:d8c0f97fc6a6ac400e43342e67d06222b27cecdb076cbf8a87f3a2a25effe81c:   done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:fc0cda0e09ab32c72c61d272bb409da4e2f73165c7bf584226880c9b85438e63:    done           |++++++++++++++++++++++++++++++++++++++|
elapsed: 83.7s

然后将该镜像重新 tag 成 Harbor 上的镜像地址:

$ nerdctl tag busybox:1.35.0 harbor.k8s.local/library/busybox:1.35.0

再执行 push 命令即可将镜像推送到 Harbor 上:

$ nerdctl push --insecure-registry harbor.k8s.local/library/busybox:1.35.0
INFO[0000] pushing as a reduced-platform image (application/vnd.docker.distribution.manifest.list.v2+json, sha256:29fe0126b13c3ea2641ca42c450fa69583d212dbd9b7b623814977b5b0945726)
WARN[0000] skipping verifying HTTPS certs for "harbor.k8s.local"
index-sha256:29fe0126b13c3ea2641ca42c450fa69583d212dbd9b7b623814977b5b0945726:    done           |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:8cde9b8065696b65d7b7ffaefbab0262d47a5a9852bfd849799559d296d2e0cd: done           |++++++++++++++++++++++++++++++++++++++|
config-sha256:d8c0f97fc6a6ac400e43342e67d06222b27cecdb076cbf8a87f3a2a25effe81c:   done           |++++++++++++++++++++++++++++++++++++++|
elapsed: 6.9 s                                                                    total:  2.2 Ki (333.0 B/s)

推送完成后,我们就可以在 Portal 页面上看到这个镜像的信息了:

镜像 push 成功,同样可以测试下 pull:

$ nerdctl rmi harbor.k8s.local/library/busybox:1.35.0
Untagged: harbor.k8s.local/library/busybox:1.35.0@sha256:8c40df61d40166f5791f44b3d90b77b4c7f59ed39a992fd9046886d3126ffa68
Deleted: sha256:cf4ac4fc01444f1324571ceb0d4f175604a8341119d9bb42bc4b2cb431a7f3a5
$ nerdctl rmi busybox:1.35.0
Untagged: docker.io/library/busybox:1.35.0@sha256:8c40df61d40166f5791f44b3d90b77b4c7f59ed39a992fd9046886d3126ffa68
Deleted: sha256:cf4ac4fc01444f1324571ceb0d4f175604a8341119d9bb42bc4b2cb431a7f3a5
$ nerdctl pull --insecure-registry  harbor.k8s.local/library/busybox:1.35.0
WARN[0000] skipping verifying HTTPS certs for "harbor.k8s.local"
harbor.k8s.local/library/busybox:1.35.0:                                          resolved       |++++++++++++++++++++++++++++++++++++++|
index-sha256:29fe0126b13c3ea2641ca42c450fa69583d212dbd9b7b623814977b5b0945726:    done           |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:8cde9b8065696b65d7b7ffaefbab0262d47a5a9852bfd849799559d296d2e0cd: done           |++++++++++++++++++++++++++++++++++++++|
config-sha256:d8c0f97fc6a6ac400e43342e67d06222b27cecdb076cbf8a87f3a2a25effe81c:   done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:fc0cda0e09ab32c72c61d272bb409da4e2f73165c7bf584226880c9b85438e63:    done           |++++++++++++++++++++++++++++++++++++++|
elapsed: 0.7 s                                                                    total:  2.2 Ki (3.2 KiB/s)

$ nerdctl images
REPOSITORY                          TAG       IMAGE ID        CREATED           PLATFORM       SIZE         BLOB SIZE
harbor.k8s.local/library/busybox    1.35.0    29fe0126b13c    17 seconds ago    linux/amd64    1.2 MiB      757.7 KiB

但是上面我们也可以看到单独使用 containerd 比如通过 nerdctl 或者 ctr 命令访问 Harbor 镜像仓库的时候即使跳过证书校验或者配置上 CA 证书也是会出现证书错误的,这个时候我们需要去跳过证书校验或者指定证书路径才行。

# 解决办法1.指定 -k 参数跳过证书校验。
$ ctr images pull --user admin:Harbor12345 -k harbor.k8s.local/library/busybox:1.35.0

# 解决办法2.指定CA证书、Harbor 相关证书文件路径。
$ ctr images pull --user admin:Harbor12345 --tlscacert ca.crt harbor.k8s.local/library/busybox:1.35.0
ctrctl
$ crictl pull harbor.k8s.local/library/busybox@sha256:29fe0126b13c3ea2641ca42c450fa69583d212dbd9b7b623814977b5b0945726
Image is up to date for sha256:d8c0f97fc6a6ac400e43342e67d06222b27cecdb076cbf8a87f3a2a25effe81c

如果想要在 Kubernetes 中去使用那么就需要将 Harbor 的认证信息以 Secret 的形式添加到集群中去:

$ kubectl create secret docker-registry harbor-auth --docker-server=https://harbor.k8s.local --docker-username=admin --docker-password=Harbor12345 --docker-email=info@ydzs.io -n default

然后我们使用上面的私有镜像仓库来创建一个 Pod:

# test-harbor.yaml
apiVersion: v1
kind: Pod
metadata:
  name: harbor-registry-test
spec:
  containers:
  - name: test
    image: harbor.k8s.local/library/busybox:1.35.0
    args:
    - sleep
    - "3600"
  imagePullSecrets:
  - name: harbor-auth

创建后可以查看该 Pod 是否能正常获取镜像:

$ kubectl describe pod harbor-registry-test
Name:         harbor-registry-test
Namespace:    default
Priority:     0
Node:         node1/192.168.0.107
Start Time:   Thu, 07 Jul 2022 18:52:39 +0800
# ......
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  10s   default-scheduler  Successfully assigned default/harbor-registry-test to node1
  Normal  Pulling    10s   kubelet            Pulling image "harbor.k8s.local/library/busybox:1.35.0"
  Normal  Pulled     5s    kubelet            Successfully pulled image "harbor.k8s.local/library/busybox:1.35.0" in 4.670528883s
  Normal  Created    5s    kubelet            Created container test
  Normal  Started    5s    kubelet            Started container test

到这里证明上面我们的私有镜像仓库搭建成功了,大家可以尝试去创建一个私有的项目,然后创建一个新的用户,使用这个用户来进行 pull/push 镜像,Harbor 还具有其他的一些功能,比如镜像复制,Helm Chart 包托管等等,大家可以自行测试,感受下 Harbor 和官方自带的 registry 仓库的差别。